Skip to content

Commit 1eff550

Browse files
authored
Merge pull request #139 from joserafa11/add-eager-load-feature
Issue-138 Add Eager Load Option
2 parents d935131 + 98e02be commit 1eff550

File tree

7 files changed

+167
-15
lines changed

7 files changed

+167
-15
lines changed

Gemfile.lock

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ GEM
3939
method_source (1.0.0)
4040
mini_portile2 (2.8.4)
4141
minitest (5.19.0)
42+
nokogiri (1.14.0-aarch64-linux)
43+
racc (~> 1.4)
4244
nokogiri (1.15.4)
4345
mini_portile2 (~> 2.8.2)
4446
racc (~> 1.4)
@@ -67,6 +69,7 @@ GEM
6769
zeitwerk (2.6.11)
6870

6971
PLATFORMS
72+
aarch64-linux
7073
ruby
7174

7275
DEPENDENCIES

README.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,25 @@ GlobalID::Locator.locate_many gids
161161

162162
Note the order is maintained in the returned results.
163163

164+
### Options
165+
166+
Either `GlobalID::Locator.locate` or `GlobalID::Locator.locate_many` supports a hash of options as second parameter. The supported options are:
167+
168+
* :includes - A Symbol, Array, Hash or combination of them
169+
The same structure you would pass into a `includes` method of Active Record.
170+
See [Active Record eager loading associations](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations)
171+
If present, `locate` or `locate_many` will eager load all the relationships specified here.
172+
Note: It only works if all the gids models have that relationships.
173+
* :only - A class, module or Array of classes and/or modules that are
174+
allowed to be located. Passing one or more classes limits instances of returned
175+
classes to those classes or their subclasses. Passing one or more modules in limits
176+
instances of returned classes to those including that module. If no classes or
177+
modules match, +nil+ is returned.
178+
* :ignore_missing (Only for `locate_many`) - By default, `locate_many` will call `#find` on the model to locate the
179+
ids extracted from the GIDs. In Active Record (and other data stores following the same pattern),
180+
`#find` will raise an exception if a named ID can't be found. When you set this option to true,
181+
we will use `#where(id: ids)` instead, which does not raise on missing records.
182+
164183
### Custom App Locator
165184

166185
A custom locator can be set for an app by calling `GlobalID::Locator.use` and providing an app locator to use for that app.
@@ -172,7 +191,7 @@ A custom locator can either be a block or a class.
172191
Using a block:
173192

174193
```ruby
175-
GlobalID::Locator.use :foo do |gid|
194+
GlobalID::Locator.use :foo do |gid, options|
176195
FooRemote.const_get(gid.model_name).find(gid.model_id)
177196
end
178197
```
@@ -182,7 +201,7 @@ Using a class:
182201
```ruby
183202
GlobalID::Locator.use :bar, BarLocator.new
184203
class BarLocator
185-
def locate(gid)
204+
def locate(gid, options = {})
186205
@search_client.search name: gid.model_name, id: gid.model_id
187206
end
188207
end

lib/global_id.rb

+4
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ def self.eager_load!
1616
super
1717
require 'global_id/signed_global_id'
1818
end
19+
20+
def self.deprecator # :nodoc:
21+
@deprecator ||= ActiveSupport::Deprecation.new("2.1", "GlobalID")
22+
end
1923
end

lib/global_id/locator.rb

+43-11
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,27 @@ class << self
88
# Takes either a GlobalID or a string that can be turned into a GlobalID
99
#
1010
# Options:
11+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them.
12+
# The same structure you would pass into a +includes+ method of Active Record.
13+
# If present, locate will load all the relationships specified here.
14+
# See https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations.
1115
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
1216
# allowed to be located. Passing one or more classes limits instances of returned
1317
# classes to those classes or their subclasses. Passing one or more modules in limits
1418
# instances of returned classes to those including that module. If no classes or
1519
# modules match, +nil+ is returned.
1620
def locate(gid, options = {})
17-
if gid = GlobalID.parse(gid)
18-
locator_for(gid).locate gid if find_allowed?(gid.model_class, options[:only])
21+
gid = GlobalID.parse(gid)
22+
23+
return unless gid && find_allowed?(gid.model_class, options[:only])
24+
25+
locator = locator_for(gid)
26+
27+
if locator.method(:locate).arity == 1
28+
GlobalID.deprecator.warn "It seems your locator is defining the `locate` method only with one argument. Please make sure your locator is receiving the options argument as well, like `locate(gid, options = {})`."
29+
locator.locate(gid)
30+
else
31+
locator.locate(gid, options.except(:only))
1932
end
2033
end
2134

@@ -30,6 +43,11 @@ def locate(gid, options = {})
3043
# per model class, but still interpolate the results to match the order in which the gids were passed.
3144
#
3245
# Options:
46+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
47+
# The same structure you would pass into a includes method of Active Record.
48+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
49+
# If present, locate_many will load all the relationships specified here.
50+
# Note: It only works if all the gids models have that relationships.
3351
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
3452
# allowed to be located. Passing one or more classes limits instances of returned
3553
# classes to those classes or their subclasses. Passing one or more modules in limits
@@ -51,6 +69,10 @@ def locate_many(gids, options = {})
5169
# Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID
5270
#
5371
# Options:
72+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
73+
# The same structure you would pass into a includes method of Active Record.
74+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
75+
# If present, locate_signed will load all the relationships specified here.
5476
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
5577
# allowed to be located. Passing one or more classes limits instances of returned
5678
# classes to those classes or their subclasses. Passing one or more modules in limits
@@ -68,6 +90,11 @@ def locate_signed(sgid, options = {})
6890
# the results to match the order in which the gids were passed.
6991
#
7092
# Options:
93+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
94+
# The same structure you would pass into a includes method of Active Record.
95+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
96+
# If present, locate_many_signed will load all the relationships specified here.
97+
# Note: It only works if all the gids models have that relationships.
7198
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
7299
# allowed to be located. Passing one or more classes limits instances of returned
73100
# classes to those classes or their subclasses. Passing one or more modules in limits
@@ -84,7 +111,7 @@ def locate_many_signed(sgids, options = {})
84111
#
85112
# Using a block:
86113
#
87-
# GlobalID::Locator.use :foo do |gid|
114+
# GlobalID::Locator.use :foo do |gid, options|
88115
# FooRemote.const_get(gid.model_name).find(gid.model_id)
89116
# end
90117
#
@@ -93,7 +120,7 @@ def locate_many_signed(sgids, options = {})
93120
# GlobalID::Locator.use :bar, BarLocator.new
94121
#
95122
# class BarLocator
96-
# def locate(gid)
123+
# def locate(gid, options = {})
97124
# @search_client.search name: gid.model_name, id: gid.model_id
98125
# end
99126
# end
@@ -127,9 +154,12 @@ def normalize_app(app)
127154
@locators = {}
128155

129156
class BaseLocator
130-
def locate(gid)
157+
def locate(gid, options = {})
131158
return unless model_id_is_valid?(gid)
132-
gid.model_class.find gid.model_id
159+
model_class = gid.model_class
160+
model_class = model_class.includes(options[:includes]) if options[:includes]
161+
162+
model_class.find gid.model_id
133163
end
134164

135165
def locate_many(gids, options = {})
@@ -143,7 +173,7 @@ def locate_many(gids, options = {})
143173
records_by_model_name_and_id = {}
144174

145175
ids_by_model.each do |model, ids|
146-
records = find_records(model, ids, ignore_missing: options[:ignore_missing])
176+
records = find_records(model, ids, ignore_missing: options[:ignore_missing], includes: options[:includes])
147177

148178
records_by_id = records.index_by do |record|
149179
record.id.is_a?(Array) ? record.id.map(&:to_s) : record.id.to_s
@@ -157,6 +187,8 @@ def locate_many(gids, options = {})
157187

158188
private
159189
def find_records(model_class, ids, options)
190+
model_class = model_class.includes(options[:includes]) if options[:includes]
191+
160192
if options[:ignore_missing]
161193
model_class.where(model_class.primary_key => ids)
162194
else
@@ -170,7 +202,7 @@ def model_id_is_valid?(gid)
170202
end
171203

172204
class UnscopedLocator < BaseLocator
173-
def locate(gid)
205+
def locate(gid, options = {})
174206
unscoped(gid.model_class) { super }
175207
end
176208

@@ -194,12 +226,12 @@ def initialize(block)
194226
@locator = block
195227
end
196228

197-
def locate(gid)
198-
@locator.call(gid)
229+
def locate(gid, options = {})
230+
@locator.call(gid, options)
199231
end
200232

201233
def locate_many(gids, options = {})
202-
gids.map { |gid| locate(gid) }
234+
gids.map { |gid| locate(gid, options) }
203235
end
204236
end
205237
end

lib/global_id/railtie.rb

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ class Railtie < Rails::Railtie # :nodoc:
4242
send :extend, GlobalID::FixtureSet
4343
end
4444
end
45+
46+
initializer "web_console.deprecator" do |app|
47+
app.deprecators[:global_id] = GlobalID.deprecator if app.respond_to?(:deprecators)
48+
end
4549
end
4650
end
4751

test/cases/global_locator_test.rb

+50-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ class GlobalLocatorTest < ActiveSupport::TestCase
6464
assert_equal @gid.model_id, found.id
6565
end
6666

67+
test 'by GID with eager loading' do
68+
assert_equal Person::Child.new('1', Person.new('1')),
69+
GlobalID::Locator.locate(
70+
Person::Child.new('1', Person.new('1')).to_gid,
71+
includes: :parent
72+
)
73+
end
74+
75+
test 'by GID trying to eager load an unexisting relationship' do
76+
assert_raises StandardError do
77+
GlobalID::Locator.locate(
78+
Person::Child.new('1', Person.new('1')).to_gid,
79+
includes: :some_non_existent_relationship
80+
)
81+
end
82+
end
83+
6784
test 'by many GIDs of one class' do
6885
assert_equal [ Person.new('1'), Person.new('2') ],
6986
GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person.new('2').to_gid ])
@@ -91,6 +108,22 @@ class GlobalLocatorTest < ActiveSupport::TestCase
91108
GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person::Child.new('1').to_gid, Person.new('2').to_gid ], only: Person::Child)
92109
end
93110

111+
test 'by many GIDs with eager loading' do
112+
assert_equal [ Person::Child.new('1', Person.new('1')), Person::Child.new('2', Person.new('2')) ],
113+
GlobalID::Locator.locate_many(
114+
[ Person::Child.new('1', Person.new('1')).to_gid, Person::Child.new('2', Person.new('2')).to_gid ],
115+
includes: :parent
116+
)
117+
end
118+
119+
test 'by many GIDs trying to eager load an unexisting relationship' do
120+
assert_raises StandardError do
121+
GlobalID::Locator.locate_many(
122+
[ Person::Child.new('1', Person.new('1')).to_gid, Person::Child.new('2', Person.new('2')).to_gid ],
123+
includes: :some_non_existent_relationship
124+
)
125+
end
126+
end
94127

95128
test 'by SGID' do
96129
found = GlobalID::Locator.locate_signed(@sgid)
@@ -236,7 +269,7 @@ class GlobalLocatorTest < ActiveSupport::TestCase
236269

237270
test 'use locator with class' do
238271
class BarLocator
239-
def locate(gid); :bar; end
272+
def locate(gid, options = {}); :bar; end
240273
def locate_many(gids, options = {}); gids.map(&:model_id); end
241274
end
242275

@@ -248,6 +281,22 @@ def locate_many(gids, options = {}); gids.map(&:model_id); end
248281
end
249282
end
250283

284+
test 'use locator with class and single argument' do
285+
class DeprecatedBarLocator
286+
def locate(gid); :deprecated; end
287+
def locate_many(gids, options = {}); gids.map(&:model_id); end
288+
end
289+
290+
GlobalID::Locator.use :deprecated, DeprecatedBarLocator.new
291+
292+
with_app 'deprecated' do
293+
assert_deprecated(nil, GlobalID.deprecator) do
294+
assert_equal :deprecated, GlobalID::Locator.locate('gid://deprecated/Person/1')
295+
end
296+
assert_equal ['1', '2'], GlobalID::Locator.locate_many(['gid://deprecated/Person/1', 'gid://deprecated/Person/2'])
297+
end
298+
end
299+
251300
test 'app locator is case insensitive' do
252301
GlobalID::Locator.use :insensitive do |gid|
253302
:insensitive

test/models/person.rb

+42-1
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,45 @@ def self.find(*)
5353
end
5454
end
5555

56-
class Person::Child < Person; end
56+
class Person::ChildWithParent < Person
57+
def self.find(id_or_ids)
58+
if id_or_ids.is_a? Array
59+
ids = id_or_ids
60+
ids.collect { |id| find(id) }
61+
else
62+
id = id_or_ids
63+
64+
if id == HARDCODED_ID_FOR_MISSING_PERSON
65+
raise 'Person missing'
66+
else
67+
Person::Child.new(id, Person.new(id))
68+
end
69+
end
70+
end
71+
72+
def self.where(conditions)
73+
(conditions[:id] - [HARDCODED_ID_FOR_MISSING_PERSON]).collect do |id|
74+
Person::Child.new(id, Person.new(id))
75+
end
76+
end
77+
end
78+
79+
class Person::Child < Person
80+
attr_accessor :parent
81+
82+
def initialize(id = 1, parent = nil)
83+
@id = id
84+
@parent = parent
85+
end
86+
87+
def self.includes(relationships)
88+
return Person::ChildWithParent if relationships == :parent
89+
return self if relationships.nil?
90+
91+
raise StandardError, 'Relationship does not exist'
92+
end
93+
94+
def ==(other)
95+
other.is_a?(self.class) && id == other.try(:id) && parent == other.parent
96+
end
97+
end

0 commit comments

Comments
 (0)