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 +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"
|