When save callbacks are invoked on a top-level record with more than a single level of embedded documents, and when cascade_callbacks is true, any callbacks on children deeper than the first level are invoked twice.
Consider this reproducer:
require 'bundler/inline' gemfile do source 'https://rubygems.org' #gem 'mongoid', '~> 8.0.8' #gem 'mongoid', '~> 8.1.8' gem 'mongoid', '~> 9.0.3' end Mongoid.connect_to 'sandbox' Mongoid.purge! class Parent include Mongoid::Document embeds_many :children, as: :parent, cascade_callbacks: true before_save do puts 'parent' end end class Child include Mongoid::Document field :name, type: String embedded_in :parent embeds_many :children, as: :parent, cascade_callbacks: true before_save do puts "child: #{name}" end end parent = Parent.new(children: [ Child.new(name: 'A', children: [ Child.new(name: 'B'), Child.new(name: 'C') ]) ]) parent.save!
When parent is saved on the last line, it will emit the following output (from the callbacks):
parent A B C B C
Note that "B" and "C" are emitted twice!
This appears to be due to the following chain of events. The _mongoid_run_child_callbacks_without_around method is invoked, which builds the full list of children and grandchildren via cascadable_children. It then sends that list to _mongoid_run_child_before_callbacks.
The _mongoid_run_child_before_callbacks method then iterates over each child record that was passed in, running the before callbacks and then recursively calling itself on the cascadable children of the child. Recursively processing the children of the record is redundant in this case, because the list of children was already built from a fully-recursive traversal of the subdocument tree. It results in records being processed multiple times.
- is depended on by
-
MONGOID-5782 timeless leacks on embedded documents with timestamp
-
- Ready for Work
-