Skip to content

Commit ff0ef93

Browse files
authored
Merge pull request rails#51009 from HeyNonster/nony--add-on-all-shards
Add `.shard_keys`, `.sharded?`, & `.connected_to_all_shards` methods to AR Models
2 parents 43e2281 + 77cf5e6 commit ff0ef93

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

activerecord/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
* Add `.shard_keys`, `.sharded?`, & `.connected_to_all_shards` methods.
2+
3+
```ruby
4+
class ShardedBase < ActiveRecord::Base
5+
self.abstract_class = true
6+
7+
connects_to shards: {
8+
shard_one: { writing: :shard_one },
9+
shard_two: { writing: :shard_two }
10+
}
11+
end
12+
13+
class ShardedModel < ShardedBase
14+
end
15+
16+
ShardedModel.shard_keys => [:shard_one, :shard_two]
17+
ShardedModel.sharded? => true
18+
ShardedBase.connected_to_all_shards { ShardedModel.current_shard } => [:shard_one, :shard_two]
19+
```
20+
21+
*Nony Dutton*
22+
123
* Optimize `Relation#exists?` when records are loaded and the relation has no conditions.
224

325
This can avoid queries in some cases.

activerecord/lib/active_record/connection_handling.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def connects_to(database: {}, shards: {})
8787

8888
connections = []
8989

90+
@shard_keys = shards.keys
91+
9092
if shards.empty?
9193
shards[:default] = database
9294
end
@@ -175,6 +177,18 @@ def connected_to_many(*classes, role:, shard: nil, prevent_writes: false)
175177
connected_to_stack.pop
176178
end
177179

180+
# Passes the block to +connected_to+ for every +shard+ the
181+
# model is configured to connect to (if any), and returns the
182+
# results in an array.
183+
#
184+
# Optionally, +role+ and/or +prevent_writes+ can be passed which
185+
# will be forwarded to each +connected_to+ call.
186+
def connected_to_all_shards(role: nil, prevent_writes: false, &blk)
187+
shard_keys.map do |shard|
188+
connected_to(shard: shard, role: role, prevent_writes: prevent_writes, &blk)
189+
end
190+
end
191+
178192
# Use a specified connection.
179193
#
180194
# This method is useful for ensuring that a specific connection is
@@ -359,6 +373,14 @@ def clear_cache! # :nodoc:
359373
connection_pool.schema_cache.clear!
360374
end
361375

376+
def shard_keys
377+
connection_class_for_self.instance_variable_get(:@shard_keys) || []
378+
end
379+
380+
def sharded?
381+
shard_keys.any?
382+
end
383+
362384
private
363385
def resolve_config_for_connection(config_or_env)
364386
raise "Anonymous class is not allowed." unless name
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper"
4+
5+
module ActiveRecord
6+
class ShardsKeysTest < ActiveRecord::TestCase
7+
class UnshardedBase < ActiveRecord::Base
8+
self.abstract_class = true
9+
end
10+
11+
class UnshardedModel < UnshardedBase
12+
end
13+
14+
class ShardedBase < ActiveRecord::Base
15+
self.abstract_class = true
16+
end
17+
18+
class ShardedModel < ShardedBase
19+
end
20+
21+
def setup
22+
ActiveRecord::Base.instance_variable_set(:@shard_keys, nil)
23+
@previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
24+
25+
config = {
26+
"default_env" => {
27+
"primary" => {
28+
adapter: "sqlite3",
29+
database: ":memory:"
30+
},
31+
"shard_one" => {
32+
adapter: "sqlite3",
33+
database: ":memory:"
34+
},
35+
"shard_one_reading" => {
36+
adapter: "sqlite3",
37+
database: ":memory:"
38+
},
39+
"shard_two" => {
40+
adapter: "sqlite3",
41+
database: ":memory:"
42+
},
43+
"shard_two_reading" => {
44+
adapter: "sqlite3",
45+
database: ":memory:"
46+
},
47+
}
48+
}
49+
50+
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
51+
52+
UnshardedBase.connects_to database: { writing: :primary }
53+
54+
ShardedBase.connects_to shards: {
55+
shard_one: { writing: :shard_one, reading: :shard_one_reading },
56+
shard_two: { writing: :shard_two, reading: :shard_two_reading },
57+
}
58+
end
59+
60+
def teardown
61+
clean_up_connection_handler
62+
ActiveRecord::Base.configurations = @prev_configs
63+
ActiveRecord::Base.establish_connection(:arunit)
64+
ENV["RAILS_ENV"] = @previous_env
65+
end
66+
67+
def test_connects_to_sets_shard_keys
68+
assert_empty(ActiveRecord::Base.shard_keys)
69+
assert_equal([:shard_one, :shard_two], ShardedBase.shard_keys)
70+
end
71+
72+
def test_connects_to_sets_shard_keys_for_descendents
73+
assert_equal(ShardedBase.shard_keys, ShardedModel.shard_keys)
74+
end
75+
76+
def test_sharded?
77+
assert_not ActiveRecord::Base.sharded?
78+
assert_not UnshardedBase.sharded?
79+
assert_not UnshardedModel.sharded?
80+
81+
assert_predicate ShardedBase, :sharded?
82+
assert_predicate ShardedModel, :sharded?
83+
end
84+
85+
def test_connected_to_all_shards
86+
unsharded_results = UnshardedBase.connected_to_all_shards do
87+
UnshardedBase.connection_pool.db_config.name
88+
end
89+
90+
sharded_results = ShardedBase.connected_to_all_shards do
91+
ShardedBase.connection_pool.db_config.name
92+
end
93+
94+
assert_empty unsharded_results
95+
assert_equal(["shard_one", "shard_two"], sharded_results)
96+
end
97+
98+
def test_connected_to_all_shards_can_switch_each_to_reading_role
99+
# We teardown the shared connection pool and call .connects_to again
100+
# because .setup_shared_connection_pool overwrites our reading configs
101+
# with the writing role configs.
102+
teardown_shared_connection_pool
103+
ShardedBase.connects_to shards: {
104+
shard_one: { writing: :shard_one, reading: :shard_one_reading },
105+
shard_two: { writing: :shard_two, reading: :shard_two_reading },
106+
}
107+
108+
results = ShardedBase.connected_to_all_shards(role: :reading) do
109+
ShardedBase.connection_pool.db_config.name
110+
end
111+
112+
assert_equal(["shard_one_reading", "shard_two_reading"], results)
113+
end
114+
115+
def test_connected_to_all_shards_respects_preventing_writes
116+
assert_not ShardedBase.current_preventing_writes
117+
118+
results = ShardedBase.connected_to_all_shards(role: :writing, prevent_writes: true) do
119+
ShardedBase.current_preventing_writes
120+
end
121+
122+
assert_equal([true, true], results)
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)