dynashard 0.3.1 → 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/README.md +67 -33
- data/VERSION +1 -1
- data/dynashard.gemspec +2 -2
- data/lib/dynashard/associations.rb +19 -4
- data/lib/dynashard/connection_handler.rb +1 -1
- data/lib/dynashard/model.rb +51 -12
- data/lib/dynashard.rb +9 -1
- data/spec/associations_spec.rb +105 -21
- data/spec/db/schema.rb +27 -0
- data/spec/model_spec.rb +47 -9
- data/spec/spec_helper.rb +20 -16
- data/spec/support/factories.rb +5 -1
- data/spec/support/models.rb +24 -0
- data/spec/validation_spec.rb +2 -4
- metadata +34 -26
data/README.md
CHANGED
@@ -39,39 +39,34 @@ models may shard using different contexts.
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
association's owner.
|
42
|
+
Sharded models are returned as objects of a shard-specific subclass.
|
44
43
|
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
-
>
|
66
|
-
=>
|
50
|
+
> found_widget = Dynashard.with_context(:user => 'shard3') {Widget.find(:first)}
|
51
|
+
=> <#Dynashard::Shard2::Widget id: 4, name: "Found widget">
|
67
52
|
|
68
|
-
|
69
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
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.
|
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.
|
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-
|
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
|
8
|
-
#
|
9
|
-
#
|
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
|
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)
|
data/lib/dynashard/model.rb
CHANGED
@@ -3,18 +3,11 @@ module Dynashard
|
|
3
3
|
def self.extended(base)
|
4
4
|
base.extend(ClassMethods)
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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 =
|
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
|
data/spec/associations_spec.rb
CHANGED
@@ -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
|
-
|
106
|
-
|
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
|
-
|
109
|
-
|
125
|
+
it 'reads from the shard' do
|
126
|
+
@owner.sharded_has_many_throughs.should include(@one_of_many)
|
110
127
|
end
|
111
128
|
|
112
|
-
|
113
|
-
|
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
|
-
#
|
117
|
-
it '
|
118
|
-
|
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 '
|
138
|
+
it 'creates_other on the shard' do
|
122
139
|
lambda do
|
123
|
-
|
124
|
-
|
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 '
|
129
|
-
new_owner = Factory(:sharding_owner, :shard => @owner.shard)
|
144
|
+
it 'creates joins on the shard' do
|
130
145
|
lambda do
|
131
|
-
|
132
|
-
end.should change(
|
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
|
-
|
137
|
-
|
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
|
-
|
140
|
-
end.should change(
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
42
|
-
|
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
|
data/spec/support/factories.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Sequence for generating unique names
|
2
2
|
Factory.sequence :name do |n|
|
3
|
-
"Test
|
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
|
data/spec/support/models.rb
CHANGED
@@ -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
|
data/spec/validation_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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"
|