Skip to content

Commit

Permalink
MONGOID-5542 Prevent multiple calls of embedded docs callbacks (#5621) (
Browse files Browse the repository at this point in the history
#5626)

* Add test

* Fix double callback call

* Change test that expects the old behavior

* Add config option

* Flip the flag
  • Loading branch information
comandeo-mongo authored and jamis committed Jan 23, 2025
1 parent fbe77f8 commit 677707e
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 3 deletions.
10 changes: 10 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ module Config
end
end

# When this flag is true, callbacks for every embedded document will be
# called only once, even if the embedded document is embedded in multiple
# documents in the root document's dependencies graph.
# This will be the default in 9.0. Setting this flag to false restores the
# pre-9.0 behavior, where callbacks are called for every occurrence of an
# embedded document. The pre-9.0 behavior leads to a problem that for multi
# level nested documents callbacks are called multiple times.
# See https://jira.mongodb.org/browse/MONGOID-5542
option :prevent_multiple_calls_of_embedded_callbacks, default: false

# When this flag is true, callbacks for embedded documents will not be
# called. This is the default in 8.x, but will be changed to false in 9.0.
#
Expand Down
7 changes: 4 additions & 3 deletions lib/mongoid/interceptable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,14 @@ def _mongoid_run_child_callbacks(kind, children: nil, &block)
# @api private
def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
child, *tail = (children || cascadable_children(kind))
with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
if child.nil?
block&.call
elsif tail.empty?
child.run_callbacks(child_callback_type(kind, child), with_children: false, &block)
child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
else
child.run_callbacks(child_callback_type(kind, child), with_children: false) do
_mongoid_run_child_callbacks_with_around(kind, children: tail, &block)
child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
_mongoid_run_child_callbacks(kind, children: tail, &block)
end
end
end
Expand Down
37 changes: 37 additions & 0 deletions spec/integration/callbacks_models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,40 @@ class Building

has_and_belongs_to_many :architects, dependent: :nullify
end

class Root
include Mongoid::Document
embeds_many :embedded_once, cascade_callbacks: true
after_save :trace

attr_accessor :logger

def trace
logger << :root
end
end

class EmbeddedOnce
include Mongoid::Document
embeds_many :embedded_twice, cascade_callbacks: true
embedded_in :root
after_save :trace

attr_accessor :logger

def trace
logger << :embedded_once
end
end

class EmbeddedTwice
include Mongoid::Document
embedded_in :embedded_once
after_save :trace

attr_accessor :logger

def trace
logger << :embedded_twice
end
end
27 changes: 27 additions & 0 deletions spec/integration/callbacks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,31 @@ class PreviouslyPersistedPerson
expect { book.save! }.not_to raise_error(SystemStackError)
end
end

context 'nested embedded documents' do
config_override :prevent_multiple_calls_of_embedded_callbacks, true

let(:logger) { Array.new }

let(:root) do
Root.new(
embedded_once: [
EmbeddedOnce.new(
embedded_twice: [EmbeddedTwice.new]
)
]
)
end

before(:each) do
root.logger = logger
root.embedded_once.first.logger = logger
root.embedded_once.first.embedded_twice.first.logger = logger
end

it 'runs callbacks in the correct order' do
root.save!
expect(logger).to eq(%i[embedded_twice embedded_once root])
end
end
end
22 changes: 22 additions & 0 deletions spec/mongoid/interceptable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ class TestClass
context 'with embedded grandchildren' do
IS = InterceptableSpec

config_override :prevent_multiple_calls_of_embedded_callbacks, true

context 'when creating' do
let(:registry) { IS::CallbackRegistry.new(only: %i[ before_save ]) }

Expand Down Expand Up @@ -656,6 +658,26 @@ class TestClass
band.notes.push(note)
record.notes.push(note)
end

context "when saving the root" do
context 'with prevent_multiple_calls_of_embedded_callbacks enabled' do
config_override :prevent_multiple_calls_of_embedded_callbacks, true

it "executes the callbacks only once for each document" do
expect(note).to receive(:update_saved).once
band.save!
end
end

context 'with prevent_multiple_calls_of_embedded_callbacks disabled' do
config_override :prevent_multiple_calls_of_embedded_callbacks, false

it "executes the callbacks once for each ember" do
expect(note).to receive(:update_saved).twice
band.save!
end
end
end
end
end

Expand Down

0 comments on commit 677707e

Please sign in to comment.