Skip to content

Commit 8d3c92f

Browse files
authored
MONGOID-5836 Don't repeat callbacks for embedded grandchildren. (#5933)
* MONGOID-5836 don't explicitly process grandchildren Grandchildren are already accounted for when this is invoked, because if called when callbacks are cascading, the input is the full list of all children and grandchildren. Processing grandchildren recursively here causes them to be processed twice. * specify python version to address failing mongo-orchestration * make sure we don't break update callbacks
1 parent ec18c8b commit 8d3c92f

File tree

4 files changed

+130
-120
lines changed

4 files changed

+130
-120
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ jobs:
152152
uses: actions/checkout@v2
153153
with:
154154
submodules: recursive
155+
156+
# the default python 3.8 doesn't cut it, and causes mongo-orchestration
157+
# to fail in mongodb-labs/drivers-evergreen-tools.
158+
- uses: actions/setup-python@v5
159+
with:
160+
python-version: '3.13'
161+
155162
- id: start-mongodb
156163
name: start mongodb
157164
uses: mongodb-labs/drivers-evergreen-tools@master

lib/mongoid/interceptable.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,13 @@ def run_callbacks(kind, with_children: true, skip_if: nil, &block)
152152
# @api private
153153
def _mongoid_run_child_callbacks(kind, children: nil, &block)
154154
if Mongoid::Config.around_callbacks_for_embeds
155-
_mongoid_run_child_callbacks_with_around(kind, children: children, &block)
155+
_mongoid_run_child_callbacks_with_around(kind,
156+
children: children,
157+
&block)
156158
else
157-
_mongoid_run_child_callbacks_without_around(kind, children: children, &block)
159+
_mongoid_run_child_callbacks_without_around(kind,
160+
children: children,
161+
&block)
158162
end
159163
end
160164

@@ -235,9 +239,6 @@ def _mongoid_run_child_before_callbacks(kind, children: [], callback_list: [])
235239
return false if env.halted
236240
env.value = !env.halted
237241
callback_list << [next_sequence, env]
238-
if (grandchildren = child.send(:cascadable_children, kind))
239-
_mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list)
240-
end
241242
end
242243
callback_list
243244
end

spec/mongoid/interceptable_spec.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,84 @@ class TestClass
389389
end
390390
end
391391
end
392+
393+
context 'with embedded grandchildren' do
394+
IS = InterceptableSpec
395+
396+
context 'when creating' do
397+
let(:registry) { IS::CallbackRegistry.new(only: %i[ before_save ]) }
398+
399+
let(:expected_calls) do
400+
[
401+
# the parent
402+
[ IS::CbParent, :before_save ],
403+
404+
# the immediate child of the parent
405+
[ IS::CbCascadedNode, :before_save ],
406+
407+
# the grandchild of the parent
408+
[ IS::CbCascadedNode, :before_save ],
409+
]
410+
end
411+
412+
let!(:parent) do
413+
parent = IS::CbParent.new(registry)
414+
child = IS::CbCascadedNode.new(registry)
415+
grandchild = IS::CbCascadedNode.new(registry)
416+
417+
child.cb_cascaded_nodes = [ grandchild ]
418+
parent.cb_cascaded_nodes = [ child ]
419+
420+
parent.tap(&:save)
421+
end
422+
423+
it 'should cascade callbacks to grandchildren' do
424+
expect(registry.calls).to be == expected_calls
425+
end
426+
end
427+
428+
context 'when updating' do
429+
let(:registry) { IS::CallbackRegistry.new(only: %i[ before_update ]) }
430+
431+
let(:expected_calls) do
432+
[
433+
# the parent
434+
[ IS::CbParent, :before_update ],
435+
436+
# the immediate child of the parent
437+
[ IS::CbCascadedNode, :before_update ],
438+
439+
# the grandchild of the parent
440+
[ IS::CbCascadedNode, :before_update ],
441+
]
442+
end
443+
444+
let!(:parent) do
445+
parent = IS::CbParent.new(nil)
446+
child = IS::CbCascadedNode.new(nil)
447+
grandchild = IS::CbCascadedNode.new(nil)
448+
449+
child.cb_cascaded_nodes = [ grandchild ]
450+
parent.cb_cascaded_nodes = [ child ]
451+
452+
parent.save
453+
454+
parent.callback_registry = registry
455+
child.callback_registry = registry
456+
grandchild.callback_registry = registry
457+
458+
parent.name = 'updated'
459+
child.name = 'updated'
460+
grandchild.name = 'updated'
461+
462+
parent.tap(&:save)
463+
end
464+
465+
it 'should cascade callbacks to grandchildren' do
466+
expect(registry.calls).to be == expected_calls
467+
end
468+
end
469+
end
392470
end
393471

394472
describe ".before_destroy" do

spec/mongoid/interceptable_spec_models.rb

Lines changed: 39 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# rubocop:todo all
22
module InterceptableSpec
33
class CallbackRegistry
4-
def initialize
4+
def initialize(only: [])
55
@calls = []
6+
@only = only
67
end
78

89
def record_call(cls, cb)
10+
return unless @only.empty? || @only.any? { |pat| pat == cb }
911
@calls << [cls, cb]
1012
end
1113

@@ -16,6 +18,8 @@ module CallbackTracking
1618
extend ActiveSupport::Concern
1719

1820
included do
21+
field :name, type: String
22+
1923
%i(
2024
validation save create update
2125
).each do |what|
@@ -35,199 +39,110 @@ module CallbackTracking
3539
end
3640
end
3741
end
38-
end
39-
40-
class CbHasOneParent
41-
include Mongoid::Document
4242

43-
has_one :child, autosave: true, class_name: "CbHasOneChild", inverse_of: :parent
43+
attr_accessor :callback_registry
4444

45-
def initialize(callback_registry)
45+
def initialize(callback_registry, *args, **kwargs)
4646
@callback_registry = callback_registry
47-
super()
47+
super(*args, **kwargs)
4848
end
49+
end
4950

50-
attr_accessor :callback_registry
51-
51+
module RootInsertable
5252
def insert_as_root
5353
@callback_registry&.record_call(self.class, :insert_into_database)
5454
super
5555
end
56+
end
5657

58+
class CbHasOneParent
59+
include Mongoid::Document
5760
include CallbackTracking
61+
include RootInsertable
62+
63+
has_one :child, autosave: true, class_name: "CbHasOneChild", inverse_of: :parent
5864
end
5965

6066
class CbHasOneChild
6167
include Mongoid::Document
68+
include CallbackTracking
6269

6370
belongs_to :parent, class_name: "CbHasOneParent", inverse_of: :child
64-
65-
def initialize(callback_registry)
66-
@callback_registry = callback_registry
67-
super()
68-
end
69-
70-
attr_accessor :callback_registry
71-
72-
include CallbackTracking
7371
end
7472

7573
class CbHasManyParent
7674
include Mongoid::Document
75+
include CallbackTracking
76+
include RootInsertable
7777

7878
has_many :children, autosave: true, class_name: "CbHasManyChild", inverse_of: :parent
79-
80-
def initialize(callback_registry)
81-
@callback_registry = callback_registry
82-
super()
83-
end
84-
85-
attr_accessor :callback_registry
86-
87-
def insert_as_root
88-
@callback_registry&.record_call(self.class, :insert_into_database)
89-
super
90-
end
91-
92-
include CallbackTracking
9379
end
9480

9581
class CbHasManyChild
9682
include Mongoid::Document
83+
include CallbackTracking
9784

9885
belongs_to :parent, class_name: "CbHasManyParent", inverse_of: :children
99-
100-
def initialize(callback_registry)
101-
@callback_registry = callback_registry
102-
super()
103-
end
104-
105-
attr_accessor :callback_registry
106-
107-
include CallbackTracking
10886
end
10987

11088
class CbEmbedsOneParent
11189
include Mongoid::Document
90+
include CallbackTracking
91+
include RootInsertable
11292

11393
field :name
11494

11595
embeds_one :child, cascade_callbacks: true, class_name: "CbEmbedsOneChild", inverse_of: :parent
116-
117-
def initialize(callback_registry)
118-
@callback_registry = callback_registry
119-
super()
120-
end
121-
122-
attr_accessor :callback_registry
123-
124-
def insert_as_root
125-
@callback_registry&.record_call(self.class, :insert_into_database)
126-
super
127-
end
128-
129-
include CallbackTracking
13096
end
13197

13298
class CbEmbedsOneChild
13399
include Mongoid::Document
100+
include CallbackTracking
134101

135102
field :age
136103

137104
embedded_in :parent, class_name: "CbEmbedsOneParent", inverse_of: :child
138-
139-
def initialize(callback_registry)
140-
@callback_registry = callback_registry
141-
super()
142-
end
143-
144-
attr_accessor :callback_registry
145-
146-
include CallbackTracking
147105
end
148106

149107
class CbEmbedsManyParent
150108
include Mongoid::Document
109+
include CallbackTracking
110+
include RootInsertable
151111

152112
embeds_many :children, cascade_callbacks: true, class_name: "CbEmbedsManyChild", inverse_of: :parent
153-
154-
def initialize(callback_registry)
155-
@callback_registry = callback_registry
156-
super()
157-
end
158-
159-
attr_accessor :callback_registry
160-
161-
def insert_as_root
162-
@callback_registry&.record_call(self.class, :insert_into_database)
163-
super
164-
end
165-
166-
include CallbackTracking
167113
end
168114

169115
class CbEmbedsManyChild
170116
include Mongoid::Document
117+
include CallbackTracking
171118

172119
embedded_in :parent, class_name: "CbEmbedsManyParent", inverse_of: :children
173-
174-
def initialize(callback_registry)
175-
@callback_registry = callback_registry
176-
super()
177-
end
178-
179-
attr_accessor :callback_registry
180-
181-
include CallbackTracking
182120
end
183121

184122
class CbParent
185123
include Mongoid::Document
186-
187-
def initialize(callback_registry)
188-
@callback_registry = callback_registry
189-
super()
190-
end
191-
192-
attr_accessor :callback_registry
124+
include CallbackTracking
193125

194126
embeds_many :cb_children
195127
embeds_many :cb_cascaded_children, cascade_callbacks: true
196-
197-
include CallbackTracking
128+
embeds_many :cb_cascaded_nodes, cascade_callbacks: true, as: :parent
198129
end
199130

200131
class CbChild
201132
include Mongoid::Document
133+
include CallbackTracking
202134

203135
embedded_in :cb_parent
204-
205-
def initialize(callback_registry, options)
206-
@callback_registry = callback_registry
207-
super(options)
208-
end
209-
210-
attr_accessor :callback_registry
211-
212-
include CallbackTracking
213136
end
214137

215138
class CbCascadedChild
216139
include Mongoid::Document
140+
include CallbackTracking
217141

218142
embedded_in :cb_parent
219143

220-
def initialize(callback_registry, options)
221-
@callback_registry = callback_registry
222-
super(options)
223-
end
224-
225-
attr_accessor :callback_registry
226-
227144
before_save :test_mongoid_state
228145

229-
include CallbackTracking
230-
231146
private
232147

233148
# Helps test that cascading child callbacks have access to the Mongoid
@@ -238,6 +153,15 @@ def test_mongoid_state
238153
Mongoid::Threaded.stack('interceptable').push(self)
239154
end
240155
end
156+
157+
class CbCascadedNode
158+
include Mongoid::Document
159+
include CallbackTracking
160+
161+
embedded_in :parent, polymorphic: true
162+
163+
embeds_many :cb_cascaded_nodes, cascade_callbacks: true, as: :parent
164+
end
241165
end
242166

243167
class InterceptableBand

0 commit comments

Comments
 (0)