empty_eye 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/CHANGELOG.md +43 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +17 -0
- data/empty_eye.gemspec +32 -0
- data/lib/empty_eye/active_record/base.rb +133 -0
- data/lib/empty_eye/active_record/connection_adapter.rb +46 -0
- data/lib/empty_eye/active_record/schema_dumper.rb +18 -0
- data/lib/empty_eye/associations/builder/shard_has_one.rb +20 -0
- data/lib/empty_eye/associations/shard_association_scope.rb +79 -0
- data/lib/empty_eye/associations/shard_has_one_association.rb +35 -0
- data/lib/empty_eye/errors.rb +5 -0
- data/lib/empty_eye/persistence.rb +50 -0
- data/lib/empty_eye/primary_view_extension.rb +137 -0
- data/lib/empty_eye/relation.rb +85 -0
- data/lib/empty_eye/shard.rb +130 -0
- data/lib/empty_eye/shard_association_reflection.rb +28 -0
- data/lib/empty_eye/version.rb +9 -0
- data/lib/empty_eye/view_extension.rb +115 -0
- data/lib/empty_eye/view_extension_collection.rb +222 -0
- data/lib/empty_eye.rb +30 -0
- data/spec/configuration_spec.rb +35 -0
- data/spec/mti_crud_spec.rb +130 -0
- data/spec/mti_to_sti_to_mti_crud_spec.rb +160 -0
- data/spec/spec_helper.rb +120 -0
- data/spec/sti_to_mti_crud_spec.rb +138 -0
- data/spec/validation_spec.rb +84 -0
- metadata +160 -0
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# 0.4.0 / 2012-03-11 / Grady Griffin
|
2
|
+
|
3
|
+
* added tests for validation and configurations
|
4
|
+
* reorganized files
|
5
|
+
* added more comments
|
6
|
+
|
7
|
+
|
8
|
+
# 0.3.1 / 2012-03-11 / Grady Griffin
|
9
|
+
|
10
|
+
* Cleaned up some sti problems
|
11
|
+
* added more comments
|
12
|
+
* removed some unnecessary codez
|
13
|
+
|
14
|
+
# 0.3.0 / 2012-03-11 / Grady Griffin
|
15
|
+
|
16
|
+
* inherits validations as well
|
17
|
+
* MTI to MTI is working
|
18
|
+
* STI to STI is working
|
19
|
+
* MTI to STI to MTI is working
|
20
|
+
* bulletproofed shard system by giving it its own association classes
|
21
|
+
|
22
|
+
# 0.2.1 / 2012-03-09 / Grady Griffin
|
23
|
+
|
24
|
+
* added some reasonable defaults for shard associations
|
25
|
+
* already had CRU added D for CRUD
|
26
|
+
* updated logic to support polymorphic associations
|
27
|
+
* added crud tests
|
28
|
+
|
29
|
+
# 0.2.0 / 2012-03-09 / Grady Griffin
|
30
|
+
|
31
|
+
* revamped entire library to use associations as table shards
|
32
|
+
* general cleanup
|
33
|
+
* added more options for associations
|
34
|
+
|
35
|
+
|
36
|
+
# 0.1.0 / 2012-03-08 / Grady Griffin
|
37
|
+
|
38
|
+
* can make a simple MTI class
|
39
|
+
* modified schema dumper to omit views
|
40
|
+
|
41
|
+
# 0.0.1 / 2012-03-07 / Grady Griffin
|
42
|
+
|
43
|
+
* initial commit
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Grady Griffin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Empty Eye
|
2
|
+
|
3
|
+
ActiveRecord based MTI gem powered by database views
|
4
|
+
|
5
|
+
#Issues
|
6
|
+
|
7
|
+
* No known issues major issues; has been successful within data structures of high complexity (MTI to MTI, MTI to STI to MTI relationships)
|
8
|
+
* Not sure why but new mti instances have a id of zero; this has caused no problems so far however.
|
9
|
+
* No mechanism to change mti class table name but that is minor
|
10
|
+
* More complex testing needed to ensure reliability
|
11
|
+
* Uses ARel so should be compatible with ARel supported database that support view; only tested with MySQL
|
12
|
+
* SchemaDumper support for omitting views with databases other than MySQL is untested
|
13
|
+
|
14
|
+
Create MTI classes by renaming your base table with the core suffix and wrapping your associations in a mti\_class block
|
15
|
+
|
16
|
+
Test example from http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/ which uses mixins to accomplish MTI:
|
17
|
+
|
18
|
+
ActiveRecord::Migration.create_table :restaurants_core, :force => true do |t|
|
19
|
+
t.boolean :kids_area
|
20
|
+
t.boolean :wifi
|
21
|
+
t.integer :food_genre
|
22
|
+
t.datetime :created_at
|
23
|
+
t.datetime :updated_at
|
24
|
+
t.datetime :deleted_at
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveRecord::Migration.create_table :bars_core, :force => true do |t|
|
28
|
+
t.string :music_genre
|
29
|
+
t.string :best_nights
|
30
|
+
t.string :dress_code
|
31
|
+
t.datetime :created_at
|
32
|
+
t.datetime :updated_at
|
33
|
+
t.datetime :deleted_at
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveRecord::Migration.create_table :businesses, :force => true do |t|
|
37
|
+
t.integer :biz_id
|
38
|
+
t.string :biz_type
|
39
|
+
t.string :name
|
40
|
+
t.string :address
|
41
|
+
t.string :phone
|
42
|
+
end
|
43
|
+
|
44
|
+
class Business < ActiveRecord::Base
|
45
|
+
belongs_to :biz, :polymorphic => true
|
46
|
+
end
|
47
|
+
|
48
|
+
class Restaurant < ActiveRecord::Base
|
49
|
+
mti_class do
|
50
|
+
has_one :business, :as => :biz
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Bar < ActiveRecord::Base
|
55
|
+
mti_class(:bars_core) do
|
56
|
+
has_one :business, :as => :biz
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
For now the convention is to name the base tables with the suffix core as the view will use the rails table name
|
61
|
+
|
62
|
+
In the background the following association options are used :autosave => true, :validate => true, :dependent => :destroy
|
63
|
+
|
64
|
+
MTI associations take the only and except options to limit the inherited columns.
|
65
|
+
|
66
|
+
class SmallMechanic < ActiveRecord::Base
|
67
|
+
mti_class :mechanics_core do |t|
|
68
|
+
has_one :garage, :foreign_key => :mechanic_id, :except => 'specialty'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class TinyMechanic < ActiveRecord::Base
|
73
|
+
mti_class :mechanics_core do |t|
|
74
|
+
has_one :garage, :foreign_key => :mechanic_id, :only => 'specialty'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Validations are also inherited but only for validations for attributes/columns that are inherited
|
79
|
+
|
80
|
+
Changing or adding these options will have no effect but the MTI would be senseless without them
|
81
|
+
|
82
|
+
If the class does not descend active record the correct table will be used.
|
83
|
+
|
84
|
+
If you dont want to use the core suffix convention a table can be specified (see Bar class mti implementation)
|
85
|
+
|
86
|
+
|
87
|
+
1.9.3p0 :005 > Bar
|
88
|
+
=> Bar(id: integer, music_genre: string, best_nights: string, dress_code: string, created_at: datetime, updated_at: datetime, deleted_at: datetime, name: string, address: string, phone: string)
|
89
|
+
|
90
|
+
1.9.3p0 :006 > bar = Bar.create(:music_genre => "Latin", :best_nights => "Tuesdays", :dress_code => "casual", :address => "1904 Easy Kaley Orlando, FL 32806", :name => 'Chicos', :phone => '123456789')
|
91
|
+
=> #<Bar id: 2, music_genre: "Latin", best_nights: "Tuesdays", dress_code: "casual", created_at: "2012-03-09 18:41:17", updated_at: "2012-03-09 18:41:17", deleted_at: nil, name: "Chicos", address: "1904 Easy Kaley Orlando, FL 32806", phone: "123456789">
|
92
|
+
|
93
|
+
1.9.3p0 :008 > bar.phone = '987654321'
|
94
|
+
=> "987654321"
|
95
|
+
1.9.3p0 :009 > bar.save
|
96
|
+
=> true
|
97
|
+
|
98
|
+
1.9.3p0 :010 > bar.reload
|
99
|
+
=> #<Bar id: 2, music_genre: "Latin", best_nights: "Tuesdays", dress_code: "casual", created_at: "2012-03-09 18:41:17", updated_at: "2012-03-09 18:41:17", deleted_at: nil, name: "Chicos", address: "1904 Easy Kaley Orlando, FL 32806", phone: "987654321">
|
100
|
+
|
101
|
+
1.9.3p0 :011 > bar.destroy
|
102
|
+
=> #<Bar id: 2, music_genre: "Latin", best_nights: "Tuesdays", dress_code: "casual", created_at: "2012-03-09 18:41:17", updated_at: "2012-03-09 18:41:17", deleted_at: nil, name: "Chicos", address: "1904 Easy Kaley Orlando, FL 32806", phone: "987654321">
|
103
|
+
|
104
|
+
1.9.3p0 :013 > Bar.find_by_id(2)
|
105
|
+
=> nil
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new('spec')
|
5
|
+
|
6
|
+
# If you want to make this the default task
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc "Open an irb session preloaded with this library"
|
10
|
+
task :console do
|
11
|
+
sh "irb -rubygems -I lib -r empty_eye.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
task :test_console do
|
15
|
+
sh "irb -rubygems -I lib -r empty_eye.rb -I spec -r spec_helper.rb "
|
16
|
+
end
|
17
|
+
|
data/empty_eye.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "empty_eye/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "empty_eye"
|
7
|
+
s.version = EmptyEye::VERSION::STRING
|
8
|
+
s.authors = ["thegboat"]
|
9
|
+
s.email = ["gradygriffin@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Active Record MTI gem}
|
12
|
+
s.description = %q{Active Record MTI gem powered by database views}
|
13
|
+
|
14
|
+
s.rubyforge_project = "empty_eye"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
22
|
+
s.add_runtime_dependency('activerecord', '>= 2.3.0')
|
23
|
+
s.add_runtime_dependency('arel', '>= 3.0.0')
|
24
|
+
s.add_development_dependency("rspec")
|
25
|
+
s.add_development_dependency("mysql2")
|
26
|
+
else
|
27
|
+
s.add_dependency('activerecord', '>= 2.3.0')
|
28
|
+
s.add_dependency('arel', '>= 3.0.0')
|
29
|
+
s.add_development_dependency("rspec")
|
30
|
+
s.add_development_dependency("mysql2")
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
#am i an mti class? easier than making a new class type ... i tried
|
7
|
+
def mti_class?
|
8
|
+
extended_with.any?
|
9
|
+
end
|
10
|
+
|
11
|
+
#interface for building mti_class
|
12
|
+
#primary table is not necessary if the table named correctly (Bar => bars_core)
|
13
|
+
#OR if the class inherits a primary table
|
14
|
+
#simply wrap your greasy association in this block
|
15
|
+
def mti_class(primary_table = nil)
|
16
|
+
self.primary_key = "id"
|
17
|
+
raise(EmptyEye::AlreadyExtended, "MTI class method already invoked") if mti_class?
|
18
|
+
set_mti_primary_table(primary_table)
|
19
|
+
self.table_name = compute_view_name
|
20
|
+
extended_with.primary_table(mti_primary_table)
|
21
|
+
before_yield = reflect_on_multiple_associations(:has_one)
|
22
|
+
yield nil if block_given?
|
23
|
+
mti_associations = reflect_on_multiple_associations(:has_one) - before_yield
|
24
|
+
extend_mti_class(mti_associations)
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
#all data for mti class is stored here
|
29
|
+
#when empty it is not so MT-I
|
30
|
+
def extended_with
|
31
|
+
@extended_with ||= EmptyEye::ViewExtensionCollection.new(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
#the class of primary shard
|
35
|
+
def mti_primary_shard
|
36
|
+
extended_with.primary.shard
|
37
|
+
end
|
38
|
+
|
39
|
+
#we dont need no freakin' finder
|
40
|
+
#the view handles this
|
41
|
+
def finder_needs_type_condition?
|
42
|
+
!mti_class? and super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
#we know the associations and we know what they can do
|
48
|
+
#we will make a mti class accordingly here
|
49
|
+
def extend_mti_class(mti_associations)
|
50
|
+
mti_associations.each do |assoc|
|
51
|
+
extended_with.association(assoc)
|
52
|
+
end
|
53
|
+
create_view
|
54
|
+
reset_column_information
|
55
|
+
inherit_mti_validations
|
56
|
+
end
|
57
|
+
|
58
|
+
#we need a name for the view
|
59
|
+
#need to have a way to set this
|
60
|
+
def compute_view_name
|
61
|
+
descends_from_active_record? ? compute_table_name : name.underscore.pluralize
|
62
|
+
end
|
63
|
+
|
64
|
+
#determine the primary table
|
65
|
+
#first detrmine if our view name exists; this will need to chage one day
|
66
|
+
#if they didnt specify try using the core convention else the superclass
|
67
|
+
#if they specified use what the set
|
68
|
+
def set_mti_primary_table(primary_table_name)
|
69
|
+
@mti_primary_table = if ordinary_table_exists?
|
70
|
+
raise(EmptyEye::ViewNameError, "MTI view cannot be created because a table named '#{compute_view_name}' already exists")
|
71
|
+
elsif primary_table_name.nil?
|
72
|
+
descends_from_active_record? ? "#{compute_table_name}_core" : superclass.table_name
|
73
|
+
else
|
74
|
+
primary_table_name
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def mti_primary_table
|
79
|
+
@mti_primary_table
|
80
|
+
end
|
81
|
+
|
82
|
+
#we need this when we add new associaton types to extend with
|
83
|
+
#we could use the baked in version for now
|
84
|
+
def reflect_on_multiple_associations(*assoc_types)
|
85
|
+
assoc_types.collect do |assoc_type|
|
86
|
+
reflect_on_all_associations
|
87
|
+
end.flatten.uniq
|
88
|
+
end
|
89
|
+
|
90
|
+
#determine if what we want to name our view already exists
|
91
|
+
def ordinary_table_exists?
|
92
|
+
connection.tables_without_views.include?(compute_view_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
#drop the view; dont check if we can just rescue any errors
|
96
|
+
#create the view
|
97
|
+
def create_view
|
98
|
+
connection.execute("DROP VIEW #{table_name}") rescue nil
|
99
|
+
connection.execute(extended_with.create_view_sql)
|
100
|
+
end
|
101
|
+
|
102
|
+
#we may need to inherit these... not using for now
|
103
|
+
def superclass_extensions
|
104
|
+
superclass.extended_with.dup.descend(self) unless descends_from_active_record?
|
105
|
+
end
|
106
|
+
|
107
|
+
#we know how to rebuild the validations from the shards
|
108
|
+
#lets call our inherited validation here
|
109
|
+
def inherit_mti_validations
|
110
|
+
extended_with.validations.each {|args| send(*args)}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
#a pseudo association method back to instances primary shard
|
117
|
+
def mti_primary_shard
|
118
|
+
@mti_primary_shard ||= if new_record?
|
119
|
+
self.class.mti_primary_shard.new(:mti_instance => self)
|
120
|
+
else
|
121
|
+
rtn = self.class.mti_primary_shard.find_by_id(id)
|
122
|
+
rtn.mti_instance = self
|
123
|
+
rtn
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#is the instance an instance of mti_class?
|
128
|
+
def mti_class?
|
129
|
+
self.class.mti_class?
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class AbstractAdapter
|
4
|
+
#we need this logic to correctly build schema file
|
5
|
+
def tables_without_views
|
6
|
+
#trying to make compatible with rails 2.3.x; failing
|
7
|
+
if respond_to?('execute_and_free')
|
8
|
+
execute_and_free(tables_without_views_sql) do |result|
|
9
|
+
result.collect { |field| field.first }
|
10
|
+
end
|
11
|
+
else
|
12
|
+
result = execute(tables_without_views_sql)
|
13
|
+
rtn = result.collect { |field| field.first }
|
14
|
+
result.free rescue nil
|
15
|
+
rtn
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
#i coulda made this more OO but this seemed to handle more unknown speed bumps
|
22
|
+
#perusing rails_sql_views helped a bit
|
23
|
+
# https://github.com/aeden/rails_sql_views
|
24
|
+
def tables_without_views_sql
|
25
|
+
case self.to_s
|
26
|
+
when /^mysql/i
|
27
|
+
"SHOW FULL TABLES WHERE table_type = 'BASE TABLE'"
|
28
|
+
when /postgresql/i
|
29
|
+
%{SELECT tablename
|
30
|
+
FROM pg_tables
|
31
|
+
WHERE schemaname = ANY (current_schemas(false)) AND table_type = 'BASE TABLE'}
|
32
|
+
when /sqlite/i
|
33
|
+
%{SELECT name
|
34
|
+
FROM sqlite_master
|
35
|
+
WHERE type = 'table' AND NOT name = 'sqlite_sequence'}
|
36
|
+
when /oracle/i
|
37
|
+
"SELECT TABLE_NAME FROM USER_TABLES"
|
38
|
+
when /sqlserver/i
|
39
|
+
"SELECT table_name FROM information_schema.tables"
|
40
|
+
else
|
41
|
+
"SHOW FULL TABLES WHERE table_type = 'BASE TABLE'"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class SchemaDumper
|
3
|
+
#we dont want views in our schema file
|
4
|
+
def tables(stream)
|
5
|
+
@connection.tables_without_views.sort.each do |tbl|
|
6
|
+
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
|
7
|
+
case ignored
|
8
|
+
when String; tbl == ignored
|
9
|
+
when Regexp; tbl =~ ignored
|
10
|
+
else
|
11
|
+
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
table(tbl, stream)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
module Associations
|
3
|
+
module Builder
|
4
|
+
class ShardHasOne < ActiveRecord::Associations::Builder::HasOne
|
5
|
+
#special association builder for shard
|
6
|
+
#very verbose but will be easier to update later
|
7
|
+
#better than monkey patching
|
8
|
+
#this builder allows the other special shard association-ish classes to be created
|
9
|
+
#the ground floor ...
|
10
|
+
|
11
|
+
def build
|
12
|
+
reflection = super
|
13
|
+
configure_dependency unless options[:through]
|
14
|
+
reflection
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
module Associations
|
3
|
+
class ShardAssociationScope < ActiveRecord::Associations::AssociationScope
|
4
|
+
#special association scope for shard
|
5
|
+
#very verbose but will be easier to update later
|
6
|
+
#better than monkey patching
|
7
|
+
#here we are patching the need to set the polymorphic type for a association lookup
|
8
|
+
#the shard is the 'owner' but we want the type to be the master class
|
9
|
+
|
10
|
+
|
11
|
+
def add_constraints(scope)
|
12
|
+
tables = construct_tables
|
13
|
+
|
14
|
+
chain.each_with_index do |reflection, i|
|
15
|
+
table, foreign_table = tables.shift, tables.first
|
16
|
+
|
17
|
+
if reflection.source_macro == :has_and_belongs_to_many
|
18
|
+
join_table = tables.shift
|
19
|
+
|
20
|
+
scope = scope.joins(join(
|
21
|
+
join_table,
|
22
|
+
table[reflection.association_primary_key].
|
23
|
+
eq(join_table[reflection.association_foreign_key])
|
24
|
+
))
|
25
|
+
|
26
|
+
table, foreign_table = join_table, tables.first
|
27
|
+
end
|
28
|
+
|
29
|
+
if reflection.source_macro == :belongs_to
|
30
|
+
if reflection.options[:polymorphic]
|
31
|
+
key = reflection.association_primary_key(klass)
|
32
|
+
else
|
33
|
+
key = reflection.association_primary_key
|
34
|
+
end
|
35
|
+
|
36
|
+
foreign_key = reflection.foreign_key
|
37
|
+
else
|
38
|
+
key = reflection.foreign_key
|
39
|
+
foreign_key = reflection.active_record_primary_key
|
40
|
+
end
|
41
|
+
|
42
|
+
conditions = self.conditions[i]
|
43
|
+
|
44
|
+
if reflection == chain.last
|
45
|
+
scope = scope.where(table[key].eq(owner[foreign_key]))
|
46
|
+
|
47
|
+
if reflection.type
|
48
|
+
scope = scope.where(table[reflection.type].eq(owner.mti_master_class.base_class.name))
|
49
|
+
end
|
50
|
+
|
51
|
+
conditions.each do |condition|
|
52
|
+
if options[:through] && condition.is_a?(Hash)
|
53
|
+
condition = { table.name => condition }
|
54
|
+
end
|
55
|
+
|
56
|
+
scope = scope.where(interpolate(condition))
|
57
|
+
end
|
58
|
+
else
|
59
|
+
constraint = table[key].eq(foreign_table[foreign_key])
|
60
|
+
|
61
|
+
if reflection.type
|
62
|
+
type = chain[i + 1].klass.base_class.name
|
63
|
+
constraint = constraint.and(table[reflection.type].eq(type))
|
64
|
+
end
|
65
|
+
|
66
|
+
scope = scope.joins(join(foreign_table, constraint))
|
67
|
+
|
68
|
+
unless conditions.empty?
|
69
|
+
scope = scope.where(sanitize(conditions, table))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
scope
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
module Associations
|
3
|
+
class ShardHasOneAssociation < ActiveRecord::Associations::HasOneAssociation
|
4
|
+
#special association for shard
|
5
|
+
#very verbose but will be easier to update later
|
6
|
+
#better than monkey patching
|
7
|
+
#here we are patching the need to set the polymorphic type for a association
|
8
|
+
#the shard is the 'owner' but we want the type to be the master class
|
9
|
+
|
10
|
+
def association_scope
|
11
|
+
if klass
|
12
|
+
@association_scope ||= ShardAssociationScope.new(self).scope
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def creation_attributes
|
19
|
+
attributes = {}
|
20
|
+
|
21
|
+
if reflection.macro.in?([:has_one, :has_many]) && !options[:through]
|
22
|
+
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
|
23
|
+
|
24
|
+
if reflection.options[:as]
|
25
|
+
attributes[reflection.type] = owner.mti_master_class.base_class.name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module EmptyEye
|
2
|
+
module Persistence
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
#if it is not a mti_class do what you do
|
6
|
+
#else let the primary shard do the saving
|
7
|
+
def update(attribute_names = @attributes.keys)
|
8
|
+
return super unless mti_class?
|
9
|
+
mti_primary_shard.cascade_save
|
10
|
+
1
|
11
|
+
end
|
12
|
+
|
13
|
+
#if it is not a mti_class do what you do
|
14
|
+
#else let the primary shard do the saving
|
15
|
+
#come back and cleanup
|
16
|
+
def create
|
17
|
+
return super unless mti_class?
|
18
|
+
mti_primary_shard.cascade_save
|
19
|
+
ActiveRecord::IdentityMap.add(self) if ActiveRecord::IdentityMap.enabled?
|
20
|
+
@new_record = false
|
21
|
+
self.id
|
22
|
+
end
|
23
|
+
|
24
|
+
#if it is not a mti_class do what you do
|
25
|
+
#else let the primary shard do the destruction
|
26
|
+
#come back and cleanup
|
27
|
+
def destroy
|
28
|
+
return super unless mti_class?
|
29
|
+
mti_primary_shard.destroy
|
30
|
+
if ActiveRecord::IdentityMap.enabled? and persisted?
|
31
|
+
ActiveRecord::IdentityMap.remove(self)
|
32
|
+
end
|
33
|
+
@destroyed = true
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
#if it is not a mti_class do what you do
|
38
|
+
#else let the primary shard do the deletion
|
39
|
+
#come back and cleanup
|
40
|
+
def delete
|
41
|
+
return super unless mti_class?
|
42
|
+
self.class.delete_all(:id => id)
|
43
|
+
if ActiveRecord::IdentityMap.enabled? and persisted?
|
44
|
+
ActiveRecord::IdentityMap.remove(self)
|
45
|
+
end
|
46
|
+
@destroyed = true
|
47
|
+
freeze
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|