Skip to content

Commit a86ffec

Browse files
authored
Support both identity and equality comparison in Loader#for (#53)
1 parent 1c8f156 commit a86ffec

4 files changed

Lines changed: 47 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [Unreleased]
2+
3+
- Support both identity and equality comparison in `Loader#for`. Identity lookup (via `object_id`) is tried first, with equality lookup as a fallback. Thanks [Alfonso Uceda](https://github.com/AlfonsoUceda) for reporting the issue!
4+
15
## [2.1.0] - 2026/03/07
26

37
- Add `n1_bind_to` to support context sharing for plain Ruby objects without ActiveRecord, enabling N+1-free batch loading. Nested loading through N1Loader automatically propagates the shared context. Thanks [Pawel Pacana](https://github.com/paneq) for the feature request!

lib/n1_loader/ar_lazy_preload/loader_patch.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ module LoaderPatch
77
attr_accessor :context_setup
88

99
def loaded
10-
return @loaded if @already_loaded && @already_context
10+
return @loaded_by_identity if @already_loaded && @already_context
1111

1212
super
1313

1414
synchronize do
15-
context_setup&.call(@loaded.values.flatten) unless @already_context
15+
context_setup&.call(@loaded_by_identity.values.flatten) unless @already_context
1616
end
1717

1818
@already_context = true
19-
@loaded
19+
@loaded_by_identity
2020
end
2121
end
2222
end

lib/n1_loader/core/loader.rb

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,16 @@ def initialize(elements, **args)
4747
end
4848

4949
def for(element)
50-
if loaded.empty? && elements.any?
50+
identity_loaded = loaded
51+
52+
if identity_loaded.empty? && elements.any?
5153
raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
5254
end
53-
raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
5455

55-
loaded[element]
56+
return identity_loaded[element] if identity_loaded.key?(element)
57+
return @loaded_by_value[element] if @loaded_by_value.key?(element)
58+
59+
raise NotLoaded, "The data was not preloaded for the given element"
5660
end
5761

5862
def cache_key
@@ -103,25 +107,28 @@ def perform(_elements)
103107
end
104108

105109
def fulfill(element, value)
106-
@loaded[element] = value
110+
@loaded_by_identity[element] = value
111+
@loaded_by_value[element] = value
107112
end
108113

109-
def loaded
110-
return @loaded if @already_loaded
114+
def ensure_loaded
115+
return if @already_loaded
111116

112-
synchronize do
113-
non_thread_safe_loaded unless @already_loaded
114-
end
117+
synchronize { non_thread_safe_loaded unless @already_loaded }
118+
end
115119

116-
@loaded
120+
def loaded
121+
ensure_loaded
122+
@loaded_by_identity
117123
end
118124

119125
def non_thread_safe_loaded # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
120-
return @loaded if @already_loaded
126+
return if @already_loaded
121127

122128
check_arguments!
123129

124-
@loaded = {}
130+
@loaded_by_identity = {}.compare_by_identity
131+
@loaded_by_value = {}
125132

126133
if respond_to?(:single) && elements.size == 1
127134
fulfill(elements.first, single(elements.first))
@@ -131,7 +138,6 @@ def non_thread_safe_loaded # rubocop:disable Metrics/AbcSize, Metrics/MethodLeng
131138
end
132139

133140
@already_loaded = true
134-
@loaded
135141
end
136142
end
137143
end

spec/n1_loader_spec.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,33 @@ def perform(elements)
182182
end
183183

184184
describe "loaded comparison" do
185-
it "compares by value" do
185+
it "compares by identity first" do
186186
instance = loader.new(objects)
187187

188+
expect(objects.first).to equal(objects.first)
188189
expect(instance.for(objects.first)).to eq([objects.first])
189190

190191
expect { instance.for(object) }.to raise_error(N1Loader::NotLoaded)
191192
end
193+
194+
it "falls back to equality comparison when no identity match" do
195+
equal_klass = Struct.new(:id)
196+
197+
original = equal_klass.new(1)
198+
equal_copy = equal_klass.new(1)
199+
200+
custom_loader = Class.new(N1Loader::Loader) do
201+
def perform(elements)
202+
elements.each { |element| fulfill(element, [element]) }
203+
end
204+
end
205+
206+
instance = custom_loader.new([original])
207+
208+
expect(original).not_to equal(equal_copy)
209+
expect(instance.for(original)).to eq([original])
210+
expect(instance.for(equal_copy)).to eq([original])
211+
end
192212
end
193213

194214
context "when fulfill was not used" do

0 commit comments

Comments
 (0)