dynashard 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -39,39 +39,34 @@ models may shard using different contexts.
39
39
  end
40
40
  end
41
41
 
42
- Associated models may be configured to use different shards determined by the
43
- association's owner.
42
+ Sharded models are returned as objects of a shard-specific subclass.
44
43
 
45
- class Company < ActiveRecord::Base
46
- shard :associated, :using => :shard
47
-
48
- has_many :customers
49
-
50
- def shard
51
- # logic to find the company's shard
52
- end
53
- end
54
-
55
- class Customer < ActiveRecord::Base
56
- belongs_to :company
57
- shard :by => :company
58
- end
59
-
60
- > c = Company.find(:first)
61
- => #<Company id:1>
44
+ > new_widget = Dynashard.with_context(:user => 'shard1') {Widget.new(:name => 'New widget')}
45
+ => <#Dynashard::Shard0::Widget id: nil, name: "New widget">
62
46
 
63
- Company is loaded using the default ActiveRecord connection.
47
+ > created_widget = Dynashard.with_context(:user => 'shard2') {Widget.create(:name => 'Created widget')}
48
+ => <#Dynashard::Shard1::Widget id: 1, name: "Created widget">
64
49
 
65
- > c.customers
66
- => [#<Dynashard::Shard0::Customer id: 1>, #<Dynashard::Shard0::Customer id: 2>]
50
+ > found_widget = Dynashard.with_context(:user => 'shard3') {Widget.find(:first)}
51
+ => <#Dynashard::Shard2::Widget id: 4, name: "Found widget">
67
52
 
68
- Customers are loaded using the connection for the Company's shard. Associated models
69
- are returned as shard-specific subclasses of the association class.
53
+ > found_widgets = Dynashard.with_context(:user => 'shard3') {Widget.find(:all)}
54
+ => [<#Dynashard::Shard2::Widget id: 4, name: "Found widget">, <#Dynashard::Shard2::Widget id: 5, name: "Other found widget">]
70
55
 
71
- > c.customers.create(:name => 'Always right')
72
- => #<Dynashard::Shard0::Customer id: 3>
56
+ New objects are saved on the shard with the context that was active
57
+ when the object was initialized.
58
+
59
+ > new_widget.save
60
+ => <#Dynashard::Shard0::Widget id: 1, name: "New widget"> # saved on 'shard1'
61
+
62
+ Created and found objects are updated on the shard with the context
63
+ that was active when they were created or found.
73
64
 
74
- New associations are saved on the Company's shard.
65
+ > created_widget.update_attribute(:name, 'New name')
66
+ => true # updated on 'shard2'
67
+
68
+ > found_widget.update_attributes(:name => 'Updated name')
69
+ => true # updated on 'shard3'
75
70
 
76
71
  Shard context values may be a valid argument to establish_connection()
77
72
  such as a string reference to a configuration from config/database.yml
@@ -96,13 +91,13 @@ establish_connection().
96
91
  <<: *defaults
97
92
 
98
93
  > @widgets = Dynashard.with_context(:user => 'shard1') { Widget.find(:all) }
99
- => [#<Widget id:1>, #<Widget id:2>]
94
+ => [#<Dynashard::Shard0::Widget id:1>, #<Dynashard::Shard0::Widget id:2>]
100
95
 
101
96
  Load widgets from a shard using a hash of connection params
102
97
 
103
98
  > conn = {:adapter => 'sqlite3', :database => 'db/shard3.sqlite3'}
104
99
  > @widgets = Dynashard.with_context(:user => conn) { Widget.find(:all) }
105
- => [#<Widget id:1>, #<Widget id:2>]
100
+ => [#<Dynashard::Shard2::Widget id:1>, #<Dynashard::Shard2::Widget id:2>]
106
101
 
107
102
  Create a widget using a method to determine the shard
108
103
 
@@ -120,7 +115,7 @@ establish_connection().
120
115
  > new_widget = Dynashard.with_context(:user => widget_shard) do
121
116
  Widget.create(:name => 'The newest of the widgets')
122
117
  end
123
- => <#Widget id:3>
118
+ => <#Dynashard::Shard4::Widget id:3>
124
119
 
125
120
  Use a Rails initializer for one-time configuration of shard context
126
121
 
@@ -132,20 +127,59 @@ establish_connection().
132
127
  end
133
128
 
134
129
  > new_widget = Widget.create(:name => 'Put this on the smallest shard')
135
- => <#Widget id:4>
130
+ => <#Dynashard::Shard5::Widget id:4>
136
131
 
137
132
  Use with_context to override an earlier context setting
138
133
 
139
134
  > Dynashard.shard_context[:user] = 'shard1'
140
135
  > new_widget = Widget.create(:name => 'Put this on shard1')
141
- => <#Widget id:5>
136
+ => <#Dynashard::Shard0::Widget id:5>
142
137
  > new_widget = Dynashard.with_context(:user => 'shard2') do
143
138
  Widget.create(:name => 'Put this on shard2')
144
139
  do
145
- > <#Widget id:6>
140
+ > <#Dynashard::Shard1::Widget id:6>
141
+
142
+ Associated models may be configured to use different shards determined by the
143
+ association's owner.
144
+
145
+ class Company < ActiveRecord::Base
146
+ shard :associated, :using => :shard
147
+
148
+ has_many :customers
149
+
150
+ def shard
151
+ # logic to find the company's shard
152
+ end
153
+ end
154
+
155
+ class Customer < ActiveRecord::Base
156
+ belongs_to :company
157
+ shard :by => :company
158
+ end
159
+
160
+ Load a Company using the default ActiveRecord connection.
161
+
162
+ > c = Company.find(:first)
163
+ => #<Company id:1>
164
+
165
+ Load Customers using the connection for the Company's shard.
166
+ Associated models are returns as shard-specific subclasses of the
167
+ association class.
168
+
169
+ > c.customers
170
+ => [#<Dynashard::Shard0::Customer id: 1>, #<Dynashard::Shard0::Customer id: 2>]
171
+
172
+ Save new associations on the Company's shard.
173
+
174
+ > c.customers.create(:name => 'Always right')
175
+ => #<Dynashard::Shard0::Customer id: 3>
146
176
 
147
177
  ## TODO: add gotcha section, eg:
148
178
 
179
+ - many-to-many associations can only be used across shards in one
180
+ direction, where the association target and the join table exist
181
+ on the same database connection (else joins don't work.)
149
182
  - uniqueness validations should be scoped by whatever is sharding
150
183
  - ways to shoot yourself in the foot with non-sharding association
151
184
  owners of sharded models
185
+ - investigate proxy extend for association proxy
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.4.0
data/dynashard.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dynashard}
8
- s.version = "0.3.1"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Nick Hengeveld"]
12
- s.date = %q{2011-02-01}
12
+ s.date = %q{2011-02-14}
13
13
  s.description = %q{Dynashard allows you to shard your ActiveRecord models. Models can be configured to shard based on context that can be defined dynamically.}
14
14
  s.email = %q{nickh@verticalresponse.com}
15
15
  s.extra_rdoc_files = [
@@ -4,15 +4,30 @@ module Dynashard
4
4
  base.alias_method_chain :initialize, :dynashard
5
5
  end
6
6
 
7
- # Initialize an association proxy. If the proxy owner class is configured to
8
- # shard its associations and the reflection klass is sharded, use a custom
9
- # reflection with a sharded class.
7
+ # Initialize an association proxy. If the proxy target needs to be sharded,
8
+ # swap in a reflection with a sharded "klass" to ensure that the shard
9
+ # connection is always used to manage records in the target model.
10
10
  def initialize_with_dynashard(owner, reflection)
11
- if owner.class.shards_associated? && reflection.klass.sharding_enabled?
11
+ if needs_sharded_reflection?(owner, reflection)
12
12
  reflection = Dynashard.reflection_for(owner, reflection)
13
+ if reflection.through_reflection != false && reflection.through_reflection.klass.sharding_enabled?
14
+ reflection.instance_variable_set('@through_reflection', Dynashard.reflection_for(owner, reflection.through_reflection))
15
+ end
13
16
  end
14
17
  initialize_without_dynashard(owner, reflection)
15
18
  end
19
+
20
+ private
21
+
22
+ # The reflection needs to use a sharded model class in these situations:
23
+ # - the proxy owner shards associations, and the proxy target has sharding enabled
24
+ # - the proxy owner is a dynashard-generated model class,
25
+ # the reflection class is configured to shard,
26
+ # the proxy owner superclass and reflection klass use the same shard context
27
+ def needs_sharded_reflection?(owner, reflection)
28
+ (owner.class.shards_associated? && reflection.klass.sharding_enabled?) ||
29
+ (owner.class.dynashard_model? && reflection.klass.sharding_enabled? && owner.class.superclass.dynashard_context == reflection.klass.dynashard_context)
30
+ end
16
31
  end
17
32
  end
18
33
 
@@ -31,7 +31,7 @@ module Dynashard
31
31
  retrieve_connection_pool_without_dynashard(klass.dynashard_klass)
32
32
  elsif klass.sharding_enabled?
33
33
  spec = Dynashard.shard_context[klass.dynashard_context]
34
- raise "Missing #{klass.dynashard_context} shard context" if spec.nil?
34
+ raise "Missing #{klass.dynashard_context} shard context for #{klass.name}" if spec.nil?
35
35
  spec = spec.call if spec.respond_to?(:call)
36
36
  shard_klass = Dynashard.class_for(spec)
37
37
  retrieve_connection_pool_without_dynashard(shard_klass)
@@ -3,18 +3,11 @@ module Dynashard
3
3
  def self.extended(base)
4
4
  base.extend(ClassMethods)
5
5
 
6
- # Change ActiveRecord::Base.arel_engine to create an engine for sharded models
7
- # rather than using the ActiveRecord::Base class.
8
- base.module_eval do
9
- def self.arel_engine
10
- if sharding_enabled?
11
- Arel::Sql::Engine.new(self)
12
- elsif self == ActiveRecord::Base
13
- Arel::Table.engine
14
- else
15
- connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.arel_engine
16
- end
17
- end
6
+ class << base
7
+ alias_method_chain :new, :dynashard
8
+ alias_method_chain :instantiate, :dynashard
9
+ alias_method_chain :create, :dynashard
10
+ alias_method_chain :arel_engine, :dynashard
18
11
  end
19
12
  end
20
13
 
@@ -85,6 +78,52 @@ module Dynashard
85
78
  def dynashard_association_using
86
79
  @dynashard_association_using
87
80
  end
81
+
82
+ # For sharded models, return a Arel::Sql::Engine for the shard class rather
83
+ # than ActiveRecord::Base.
84
+ def arel_engine_with_dynashard
85
+ if sharding_enabled?
86
+ Arel::Sql::Engine.new(self)
87
+ else
88
+ arel_engine_without_dynashard
89
+ end
90
+ end
91
+
92
+ # For sharded models, return new model objects with the sharded subclass
93
+ #
94
+ # > Dynashard.with_context(:owner => 'shard1'){ShardedModel.new(attrs)}
95
+ # => <#Dynashard::Shard0::ShardedModel id:nil>
96
+ def new_with_dynashard(*args)
97
+ if sharding_enabled?
98
+ dynashard_sharded_subclass.send(:new_without_dynashard, *args)
99
+ else
100
+ new_without_dynashard(*args)
101
+ end
102
+ end
103
+
104
+ # For sharded models, return instantiated model objects with the sharded subclass
105
+ #
106
+ # > Dynashard.with_context(:owner => 'shard1'){ShardedModel.find(:first)}
107
+ # => <#Dynashard::Shard0::ShardedModel id:1>
108
+ def instantiate_with_dynashard(record)
109
+ if sharding_enabled?
110
+ dynashard_sharded_subclass.send(:instantiate_without_dynashard, record)
111
+ else
112
+ instantiate_without_dynashard(record)
113
+ end
114
+ end
115
+
116
+ # For sharded models, return created model objects with the sharded subclass
117
+ #
118
+ # > Dynashard.with_context(:owner => 'shard1'){ShardedModel.create(attrs)}
119
+ # => <#Dynashard::Shard0::ShardedModel id:2>
120
+ def create_with_dynashard(attributes = nil, &block)
121
+ if sharding_enabled?
122
+ dynashard_sharded_subclass.send(:create_without_dynashard, attributes, &block)
123
+ else
124
+ create_without_dynashard(attributes, &block)
125
+ end
126
+ end
88
127
  end
89
128
  end
90
129
  end
data/lib/dynashard.rb CHANGED
@@ -112,7 +112,11 @@ module Dynashard
112
112
  # Return a reflection with a sharded class
113
113
  def self.reflection_for(owner, reflection)
114
114
  reflection_copy = reflection.dup
115
- shard_klass = Dynashard.class_for(owner.send(owner.class.dynashard_association_using))
115
+ shard_klass = if owner.class.respond_to?(:dynashard_klass)
116
+ owner.class.dynashard_klass
117
+ else
118
+ Dynashard.class_for(owner.send(owner.class.dynashard_association_using))
119
+ end
116
120
  klass = sharded_model_class(shard_klass, reflection.klass)
117
121
  reflection_copy.instance_variable_set('@klass', klass)
118
122
  reflection_copy.instance_variable_set('@class_name', klass.name)
@@ -139,6 +143,10 @@ module Dynashard
139
143
  def self.connection
140
144
  dynashard_klass.connection
141
145
  end
146
+
147
+ def self.dynashard_context
148
+ superclass.dynashard_context
149
+ end
142
150
  end
143
151
  EOE
144
152
  klass = class_name.constantize
@@ -1,5 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
+ class SqlCounter
4
+ def initialize(connection, counter_sql)
5
+ @connection, @counter_sql = connection, counter_sql
6
+ end
7
+
8
+ def count
9
+ @connection.execute(@counter_sql).first[0]
10
+ end
11
+ end
12
+
3
13
  describe 'Dynashard::ProxyExtensions' do
4
14
  before(:each) do
5
15
  Dynashard.enable
@@ -9,6 +19,7 @@ describe 'Dynashard::ProxyExtensions' do
9
19
  before(:each) do
10
20
  @owner = Factory(:sharding_owner)
11
21
  @shard = @owner.shard_dsn
22
+ @shard_klass = Dynashard.class_for(@shard)
12
23
  end
13
24
 
14
25
  context 'and a sharding proxy target' do
@@ -102,42 +113,115 @@ describe 'Dynashard::ProxyExtensions' do
102
113
 
103
114
  context 'using a :has_many_through reflection' do
104
115
  before(:each) do
105
- # Ensure that all sharded model connections happen on the owner shard
106
- Dynashard.shard_context[:owner] = @shard
116
+ @sharded_has_many_through_klass = Dynashard.sharded_model_class(@shard_klass, ShardedHasManyThrough)
117
+ @sharded_join_klass = Dynashard.sharded_model_class(@shard_klass, ShardedJoin)
118
+ @shard_klass.connection.execute("INSERT INTO sharded_has_many_throughs (name) VALUES ('#{Factory.next :name}')")
119
+ one_of_many_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
120
+ @shard_klass.connection.execute("INSERT INTO sharded_joins (sharding_owner_id, sharded_has_many_through_id) VALUES (#{@owner.id}, #{one_of_many_id})")
121
+ join_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
122
+ @one_of_many = @sharded_has_many_through_klass.find(one_of_many_id)
123
+ end
107
124
 
108
- @one_of_many = Factory(:sharded_has_many_through, :name => "Owned by #{@owner.name}")
109
- join = Factory(:sharded_join, :sharding_owner => @owner, :sharded_has_many_through => @one_of_many)
125
+ it 'reads from the shard' do
126
+ @owner.sharded_has_many_throughs.should include(@one_of_many)
110
127
  end
111
128
 
112
- after(:each) do
113
- Dynashard.shard_context.clear
129
+ it 'destroys the model from the shard' do
130
+ lambda {@one_of_many.destroy}.should change(@sharded_has_many_through_klass, :count).by(-1)
114
131
  end
115
132
 
116
- # TODO: figure out what to do about the classes being different
117
- it 'reads from the shard' do
118
- @owner.sharded_has_many_throughs.detect{|m| m.attributes == @one_of_many.attributes}.should_not be_nil
133
+ # This class does not have :dependent => :destroy on the join
134
+ it 'leaves the join on the shard' do
135
+ lambda {@one_of_many.destroy}.should_not change(@sharded_join_klass, :count)
119
136
  end
120
137
 
121
- it 'destroys from the shard' do
138
+ it 'creates_other on the shard' do
122
139
  lambda do
123
- one_of_many = @owner.sharded_has_many_throughs.detect{|m| m.attributes == @one_of_many.attributes}
124
- one_of_many.destroy
125
- end.should change(ShardedHasManyThrough, :count).by(-1)
140
+ @owner.sharded_has_many_throughs.create(:name => Factory.next(:name))
141
+ end.should change(@sharded_has_many_through_klass, :count).by(1)
126
142
  end
127
143
 
128
- it 'creates_other on the shard' do
129
- new_owner = Factory(:sharding_owner, :shard => @owner.shard)
144
+ it 'creates joins on the shard' do
130
145
  lambda do
131
- new_owner.sharded_has_many_throughs.create(:name => "Owned by #{new_owner.name}")
132
- end.should change(ShardedHasManyThrough, :count).by(1)
146
+ @owner.sharded_has_many_throughs.create(:name => Factory.next(:name))
147
+ end.should change(@sharded_join_klass, :count).by(1)
133
148
  end
134
149
 
135
150
  it 'saves built associations on the shard' do
136
- new_owner = Factory(:sharding_owner, :shard => @owner.shard)
137
- new_one_of_many = new_owner.sharded_has_many_throughs.build(:name => "Owned by #{new_owner.name}")
151
+ new_one_of_many = @owner.sharded_has_many_throughs.build(:name => Factory.next(:name))
152
+ lambda {@owner.save}.should change(@sharded_has_many_through_klass, :count).by(1)
153
+ end
154
+
155
+ it 'saves built association joins on the shard' do
156
+ new_one_of_many = @owner.sharded_has_many_throughs.build(:name => Factory.next(:name))
157
+ lambda {@owner.save}.should change(@sharded_join_klass, :count).by(1)
158
+ end
159
+
160
+ context 'with dependent => destroy' do
161
+ before(:each) do
162
+ @sharded_dependent_has_many_through_klass = Dynashard.sharded_model_class(@shard_klass, ShardedDependentHasManyThrough)
163
+ @sharded_dependent_join_klass = Dynashard.sharded_model_class(@shard_klass, ShardedDependentJoin)
164
+ @shard_klass.connection.execute("INSERT INTO sharded_dependent_has_many_throughs (name) VALUES ('#{Factory.next :name}')")
165
+ one_of_many_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
166
+ @shard_klass.connection.execute("INSERT INTO sharded_dependent_joins (sharding_owner_id, sharded_dependent_has_many_through_id) VALUES (#{@owner.id}, #{one_of_many_id})")
167
+ join_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
168
+ @dependent_one_of_many = @sharded_dependent_has_many_through_klass.find(one_of_many_id)
169
+ end
170
+
171
+ it 'destroys the join on the shard' do
172
+ lambda {@dependent_one_of_many.destroy}.should change(@sharded_dependent_join_klass, :count).by(-1)
173
+ end
174
+ end
175
+ end
176
+
177
+ context 'using a :has_and_belongs_to_many reflection' do
178
+ before(:each) do
179
+ @sharded_habtm_klass = Dynashard.sharded_model_class(@shard_klass, ShardedHabtm)
180
+ @shard_klass.connection.execute("INSERT INTO sharded_habtms (name) VALUES ('#{Factory.next :name}')")
181
+ habtm_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
182
+ @shard_klass.connection.execute("INSERT INTO sharded_habtms_sharding_owners (sharding_owner_id, sharded_habtm_id) VALUES (#{@owner.id}, #{habtm_id})")
183
+ @habtm = @sharded_habtm_klass.find(habtm_id)
184
+ @join_counter = SqlCounter.new(@shard_klass.connection, 'SELECT COUNT(*) FROM sharded_habtms_sharding_owners')
185
+ end
186
+
187
+ it 'reads from the shard' do
188
+ @owner.sharded_habtms.should include(@habtm)
189
+ end
190
+
191
+ it 'destroys the model from the shard' do
192
+ lambda {@habtm.destroy}.should change(@sharded_habtm_klass, :count).by(-1)
193
+ end
194
+
195
+ # Rails seems to always use the ActiveRecord::Base connection
196
+ # for habtm join tables
197
+ xit 'destroys the join on the shard' do
198
+ lambda {@habtm.destroy}.should change(@join_counter, :count).by(-1)
199
+ end
200
+
201
+ it 'creates_other on the shard' do
138
202
  lambda do
139
- new_one_of_many.save
140
- end.should change(ShardedHasManyThrough, :count).by(1)
203
+ @owner.sharded_habtms.create(:name => Factory.next(:name))
204
+ end.should change(@sharded_habtm_klass, :count).by(1)
205
+ end
206
+
207
+ # Rails seems to always use the ActiveRecord::Base connection
208
+ # for habtm join tables
209
+ xit 'creates joins on the shard' do
210
+ lambda do
211
+ @owner.sharded_habtms.create(:name => Factory.next(:name))
212
+ end.should change(@join_counter, :count).by(1)
213
+ end
214
+
215
+ it 'saves built associations on the shard' do
216
+ new_habtm = @owner.sharded_habtms.build(:name => Factory.next(:name))
217
+ lambda {@owner.save}.should change(@sharded_habtm_klass, :count).by(1)
218
+ end
219
+
220
+ # Rails seems to always use the ActiveRecord::Base connection
221
+ # for habtm join tables
222
+ xit 'saves built association joins on the shard' do
223
+ new_habtm = @owner.sharded_habtms.build(:name => Factory.next(:name))
224
+ lambda {@owner.save}.should change(@join_counter, :count).by(1)
141
225
  end
142
226
  end
143
227
  end
data/spec/db/schema.rb CHANGED
@@ -15,23 +15,27 @@ ActiveRecord::Schema.define(:version => 1) do
15
15
  create_table "non_sharding_owners", :force => true do |t|
16
16
  t.string "name"
17
17
  end
18
+ add_index :non_sharding_owners, :name, :unique => true
18
19
 
19
20
  create_table "sharding_owners", :force => true do |t|
20
21
  t.string "name"
21
22
  t.integer "shard_id"
22
23
  end
24
+ add_index :sharding_owners, :name, :unique => true
23
25
 
24
26
  create_table "non_sharded_has_ones", :force => true do |t|
25
27
  t.integer "sharding_owner_id"
26
28
  t.integer "non_sharding_owner_id"
27
29
  t.string "name"
28
30
  end
31
+ add_index :non_sharded_has_ones, :name, :unique => true
29
32
 
30
33
  create_table "non_sharded_has_manies", :force => true do |t|
31
34
  t.integer "sharding_owner_id"
32
35
  t.integer "non_sharding_owner_id"
33
36
  t.string "name"
34
37
  end
38
+ add_index :non_sharded_has_manies, :name, :unique => true
35
39
 
36
40
  create_table "non_sharded_joins", :force => true do |t|
37
41
  t.integer "sharding_owner_id"
@@ -42,18 +46,21 @@ ActiveRecord::Schema.define(:version => 1) do
42
46
  create_table "non_sharded_has_many_throughs", :force => true do |t|
43
47
  t.string "name"
44
48
  end
49
+ add_index :non_sharded_has_many_throughs, :name, :unique => true
45
50
 
46
51
  create_table "sharded_has_ones", :force => true do |t|
47
52
  t.integer "sharding_owner_id"
48
53
  t.integer "non_sharding_owner_id"
49
54
  t.string "name"
50
55
  end
56
+ add_index :sharded_has_ones, :name, :unique => true
51
57
 
52
58
  create_table "sharded_has_manies", :force => true do |t|
53
59
  t.integer "sharding_owner_id"
54
60
  t.integer "non_sharding_owner_id"
55
61
  t.string "name"
56
62
  end
63
+ add_index :sharded_has_manies, :name, :unique => true
57
64
 
58
65
  create_table "sharded_joins", :force => true do |t|
59
66
  t.integer "sharding_owner_id"
@@ -64,4 +71,24 @@ ActiveRecord::Schema.define(:version => 1) do
64
71
  create_table "sharded_has_many_throughs", :force => true do |t|
65
72
  t.string "name"
66
73
  end
74
+ add_index :sharded_has_many_throughs, :name, :unique => true
75
+
76
+ create_table "sharded_dependent_joins", :force => true do |t|
77
+ t.integer "sharding_owner_id"
78
+ t.integer "non_sharding_owner_id"
79
+ t.integer "sharded_dependent_has_many_through_id"
80
+ end
81
+
82
+ create_table "sharded_dependent_has_many_throughs", :force => true do |t|
83
+ t.string "name"
84
+ end
85
+ add_index :sharded_dependent_has_many_throughs, :name, :unique => true
86
+
87
+ # create_table "sharded_habtms_sharding_owners", :id => false # doesn't seem to create a schema without an ID...
88
+ connection.execute('CREATE TABLE "sharded_habtms_sharding_owners" ("sharded_habtm_id" integer, "sharding_owner_id" integer)')
89
+
90
+ create_table "sharded_habtms", :force => true do |t|
91
+ t.string "name"
92
+ end
93
+ add_index :sharded_habtms, :name, :unique => true
67
94
  end
data/spec/model_spec.rb CHANGED
@@ -88,15 +88,53 @@ describe 'ActiveRecord Models' do
88
88
  end
89
89
 
90
90
  context 'and the shard context defined' do
91
- it 'uses the sharded connection' do
92
- test_shard = 'shard1'
93
- Dynashard.with_context(:owner => test_shard) do
94
- shard_class = Dynashard.class_for(test_shard)
95
- shard_config = shard_class.connection.instance_variable_get('@config')
96
- ar_config = ActiveRecord::Base.connection.instance_variable_get('@config')
97
- model_config = ShardedHasOne.connection.instance_variable_get('@config')
98
- model_config.should_not == ar_config
99
- model_config.should == shard_config
91
+ context 'when managing sharded models' do
92
+ before(:each) do
93
+ @shard1_klass = Dynashard.class_for('shard1')
94
+ @shard2_klass = Dynashard.class_for('shard2')
95
+ @shard1_subclass = Dynashard.sharded_model_class(@shard1_klass, ShardedHasOne)
96
+ @shard2_subclass = Dynashard.sharded_model_class(@shard2_klass, ShardedHasOne)
97
+ end
98
+
99
+ it 'uses the sharded connection for the model class' do
100
+ test_shard = 'shard1'
101
+ Dynashard.with_context(:owner => test_shard) do
102
+ shard_class = Dynashard.class_for(test_shard)
103
+ shard_config = shard_class.connection.instance_variable_get('@config')
104
+ ar_config = ActiveRecord::Base.connection.instance_variable_get('@config')
105
+ model_config = ShardedHasOne.connection.instance_variable_get('@config')
106
+ model_config.should_not == ar_config
107
+ model_config.should == shard_config
108
+ end
109
+ end
110
+
111
+ it 'associates new models with the correct shard' do
112
+ new1 = Dynashard.with_context(:owner => 'shard1') {ShardedHasOne.new(:name => Factory.next(:name))}
113
+ new2 = Dynashard.with_context(:owner => 'shard2') {ShardedHasOne.new(:name => Factory.next(:name))}
114
+ new1.should be_a(@shard1_subclass)
115
+ new2.should be_a(@shard2_subclass)
116
+ new1.connection.should == @shard1_klass.connection
117
+ new2.connection.should == @shard2_klass.connection
118
+ end
119
+
120
+ it 'associates created models with the correct shard' do
121
+ created1 = Dynashard.with_context(:owner => 'shard1') {ShardedHasOne.create(:name => Factory.next(:name))}
122
+ created2 = Dynashard.with_context(:owner => 'shard2') {ShardedHasOne.create(:name => Factory.next(:name))}
123
+ created1.should be_a(@shard1_subclass)
124
+ created2.should be_a(@shard2_subclass)
125
+ created1.connection.should == @shard1_klass.connection
126
+ created2.connection.should == @shard2_klass.connection
127
+ end
128
+
129
+ it 'associates loaded models with the correct shard' do
130
+ model1 = Dynashard.with_context(:owner => 'shard1') {Factory(:sharded_has_one)}
131
+ model2 = Dynashard.with_context(:owner => 'shard2') {Factory(:sharded_has_one)}
132
+ found1 = Dynashard.with_context(:owner => 'shard1') {ShardedHasOne.find(model1.id)}
133
+ found2 = Dynashard.with_context(:owner => 'shard2') {ShardedHasOne.find(model2.id)}
134
+ found1.should be_a(@shard1_subclass)
135
+ found2.should be_a(@shard2_subclass)
136
+ found1.connection.should == @shard1_klass.connection
137
+ found2.connection.should == @shard2_klass.connection
100
138
  end
101
139
  end
102
140
  end
data/spec/spec_helper.rb CHANGED
@@ -19,27 +19,31 @@ ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir +
19
19
  ActiveRecord::Base.establish_connection("test")
20
20
  ActiveRecord::Migration.verbose = false
21
21
 
22
- test_database = ActiveRecord::Base.configurations['test']['database']
23
- File.unlink(test_database) if File.exists?(test_database)
24
- load(File.join(plugin_test_dir, "db", "schema.rb"))
25
-
26
- # create shards and databases that they point to
27
- base_config = ActiveRecord::Base.configurations['test']
28
- %w{shard1 shard2 shard3}.each do |shard|
29
- database = "#{plugin_test_dir}/db/#{shard}.sqlite3"
30
- File.unlink(database) if File.exists?(database)
31
- shard_config = base_config.merge('database' => database)
32
- ActiveRecord::Base.configurations['test'] = shard_config
33
- ActiveRecord::Base.establish_connection("test")
22
+ unless __FILE__ == "(irb)"
23
+ test_database = ActiveRecord::Base.configurations['test']['database']
24
+ File.unlink(test_database) if File.exists?(test_database)
34
25
  load(File.join(plugin_test_dir, "db", "schema.rb"))
26
+
27
+ # create shards and databases that they point to
28
+ base_config = ActiveRecord::Base.configurations['test']
29
+ %w{shard1 shard2 shard3}.each do |shard|
30
+ database = "#{plugin_test_dir}/db/#{shard}.sqlite3"
31
+ File.unlink(database) if File.exists?(database)
32
+ shard_config = base_config.merge('database' => database)
33
+ ActiveRecord::Base.configurations['test'] = shard_config
34
+ ActiveRecord::Base.establish_connection("test")
35
+ load(File.join(plugin_test_dir, "db", "schema.rb"))
36
+ end
37
+ ActiveRecord::Base.configurations['test'] = base_config
38
+ ActiveRecord::Base.establish_connection("test")
35
39
  end
36
- ActiveRecord::Base.configurations['test'] = base_config
37
- ActiveRecord::Base.establish_connection("test")
38
40
 
39
41
  require 'support/models'
40
42
 
41
- %w{shard1 shard2 shard3}.each do |shard|
42
- Shard.create(:adapter => 'sqlite3', :database => "#{plugin_test_dir}/db/#{shard}.sqlite3")
43
+ unless __FILE__ == "(irb)"
44
+ %w{shard1 shard2 shard3}.each do |shard|
45
+ Shard.create(:adapter => 'sqlite3', :database => "#{plugin_test_dir}/db/#{shard}.sqlite3")
46
+ end
43
47
  end
44
48
 
45
49
  # This has to happen after the models have been defined and the shards have been created
@@ -1,6 +1,6 @@
1
1
  # Sequence for generating unique names
2
2
  Factory.sequence :name do |n|
3
- "Test Owner #{n}"
3
+ "Test Name #{n}"
4
4
  end
5
5
 
6
6
  Factory.define(:sharding_owner) do |owner|
@@ -30,3 +30,7 @@ end
30
30
  Factory.define(:sharded_has_many_through) do |one_of_many|
31
31
  one_of_many.name {Factory.next :name}
32
32
  end
33
+
34
+ Factory.define(:sharded_habtm) do |habtm|
35
+ habtm.name {Factory.next :name}
36
+ end
@@ -39,6 +39,8 @@ class ShardingOwner < ActiveRecord::Base
39
39
  has_many :sharded_has_manys
40
40
  has_many :sharded_joins
41
41
  has_many :sharded_has_many_throughs, :through => :sharded_joins
42
+ has_many :sharded_dependent_has_many_throughs, :through => :sharded_dependent_joins
43
+ has_and_belongs_to_many :sharded_habtms
42
44
  end
43
45
 
44
46
  # Non-sharded has_one association class
@@ -97,3 +99,25 @@ class ShardedHasManyThrough < ActiveRecord::Base
97
99
 
98
100
  has_many :sharded_joins
99
101
  end
102
+
103
+ # Join table for has_many :through with dependent => destroy
104
+ class ShardedDependentJoin < ActiveRecord::Base
105
+ shard :by => :owner
106
+
107
+ belongs_to :sharding_owner
108
+ belongs_to :sharded_dependent_has_many_through
109
+ end
110
+
111
+ # Sharded has_many :through association class with dependent => destroy
112
+ class ShardedDependentHasManyThrough < ActiveRecord::Base
113
+ shard :by => :owner
114
+
115
+ has_many :sharded_dependent_joins, :dependent => :destroy
116
+ end
117
+
118
+ # Sharded habtm association class
119
+ class ShardedHabtm < ActiveRecord::Base
120
+ shard :by => :owner
121
+
122
+ has_and_belongs_to_many :sharding_owners
123
+ end
@@ -79,9 +79,8 @@ describe 'Dynashard::ValidationExtensions' do
79
79
  context 'with a conflicting record on the shard' do
80
80
  it 'returns invalid' do
81
81
  conflicting_record = Dynashard.with_context(:owner => @owner.shard_dsn){Factory(:sharded_has_one)}
82
- new_record = ShardedHasOne.new(:name => conflicting_record.name)
83
82
  Dynashard.with_context(:owner => @owner.shard_dsn) do
84
- new_record.should_not be_valid
83
+ ShardedHasOne.new(:name => conflicting_record.name).should_not be_valid
85
84
  end
86
85
  end
87
86
  end
@@ -90,9 +89,8 @@ describe 'Dynashard::ValidationExtensions' do
90
89
  it 'returns valid' do
91
90
  other_shard = Shard.find(:all).detect{|shard| shard != @owner.shard}
92
91
  non_conflicting_record = Dynashard.with_context(:owner => other_shard.dsn){Factory(:sharded_has_one)}
93
- new_record = ShardedHasOne.new(:name => non_conflicting_record.name)
94
92
  Dynashard.with_context(:owner => @owner.shard_dsn) do
95
- new_record.should be_valid
93
+ ShardedHasOne.new(:name => non_conflicting_record.name).should be_valid
96
94
  end
97
95
  end
98
96
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynashard
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 15
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 3
8
- - 1
9
- version: 0.3.1
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Nick Hengeveld
@@ -14,93 +15,99 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2011-02-01 00:00:00 -08:00
18
+ date: 2011-02-14 00:00:00 -08:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
22
+ type: :runtime
23
+ prerelease: false
21
24
  name: activerecord
22
- requirement: &id001 !ruby/object:Gem::Requirement
25
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
26
  none: false
24
27
  requirements:
25
28
  - - ">="
26
29
  - !ruby/object:Gem::Version
30
+ hash: 7
27
31
  segments:
28
32
  - 3
29
33
  - 0
30
34
  version: "3.0"
31
- type: :runtime
32
- prerelease: false
33
- version_requirements: *id001
35
+ requirement: *id001
34
36
  - !ruby/object:Gem::Dependency
37
+ type: :development
38
+ prerelease: false
35
39
  name: shoulda
36
- requirement: &id002 !ruby/object:Gem::Requirement
40
+ version_requirements: &id002 !ruby/object:Gem::Requirement
37
41
  none: false
38
42
  requirements:
39
43
  - - ">="
40
44
  - !ruby/object:Gem::Version
45
+ hash: 3
41
46
  segments:
42
47
  - 0
43
48
  version: "0"
49
+ requirement: *id002
50
+ - !ruby/object:Gem::Dependency
44
51
  type: :development
45
52
  prerelease: false
46
- version_requirements: *id002
47
- - !ruby/object:Gem::Dependency
48
53
  name: bundler
49
- requirement: &id003 !ruby/object:Gem::Requirement
54
+ version_requirements: &id003 !ruby/object:Gem::Requirement
50
55
  none: false
51
56
  requirements:
52
57
  - - ~>
53
58
  - !ruby/object:Gem::Version
59
+ hash: 23
54
60
  segments:
55
61
  - 1
56
62
  - 0
57
63
  - 0
58
64
  version: 1.0.0
65
+ requirement: *id003
66
+ - !ruby/object:Gem::Dependency
59
67
  type: :development
60
68
  prerelease: false
61
- version_requirements: *id003
62
- - !ruby/object:Gem::Dependency
63
69
  name: jeweler
64
- requirement: &id004 !ruby/object:Gem::Requirement
70
+ version_requirements: &id004 !ruby/object:Gem::Requirement
65
71
  none: false
66
72
  requirements:
67
73
  - - ~>
68
74
  - !ruby/object:Gem::Version
75
+ hash: 7
69
76
  segments:
70
77
  - 1
71
78
  - 5
72
79
  - 2
73
80
  version: 1.5.2
81
+ requirement: *id004
82
+ - !ruby/object:Gem::Dependency
74
83
  type: :development
75
84
  prerelease: false
76
- version_requirements: *id004
77
- - !ruby/object:Gem::Dependency
78
85
  name: rcov
79
- requirement: &id005 !ruby/object:Gem::Requirement
86
+ version_requirements: &id005 !ruby/object:Gem::Requirement
80
87
  none: false
81
88
  requirements:
82
89
  - - ">="
83
90
  - !ruby/object:Gem::Version
91
+ hash: 3
84
92
  segments:
85
93
  - 0
86
94
  version: "0"
87
- type: :development
88
- prerelease: false
89
- version_requirements: *id005
95
+ requirement: *id005
90
96
  - !ruby/object:Gem::Dependency
97
+ type: :runtime
98
+ prerelease: false
91
99
  name: activerecord
92
- requirement: &id006 !ruby/object:Gem::Requirement
100
+ version_requirements: &id006 !ruby/object:Gem::Requirement
93
101
  none: false
94
102
  requirements:
95
103
  - - ">="
96
104
  - !ruby/object:Gem::Version
105
+ hash: 7
97
106
  segments:
98
107
  - 3
99
108
  - 0
100
109
  version: "3.0"
101
- type: :runtime
102
- prerelease: false
103
- version_requirements: *id006
110
+ requirement: *id006
104
111
  description: Dynashard allows you to shard your ActiveRecord models. Models can be configured to shard based on context that can be defined dynamically.
105
112
  email: nickh@verticalresponse.com
106
113
  executables: []
@@ -148,7 +155,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
155
  requirements:
149
156
  - - ">="
150
157
  - !ruby/object:Gem::Version
151
- hash: 1766305880459043419
158
+ hash: 3
152
159
  segments:
153
160
  - 0
154
161
  version: "0"
@@ -157,6 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
164
  requirements:
158
165
  - - ">="
159
166
  - !ruby/object:Gem::Version
167
+ hash: 3
160
168
  segments:
161
169
  - 0
162
170
  version: "0"