Uploaded image for project: 'Mongoid'
  1. Mongoid
  2. MONGOID-5836

Duplicate save callbacks on deeply nested children

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Critical - P2 Critical - P2
    • 8.0.9, 8.1.9, 9.0.5
    • Affects Version/s: 8.0.8, 9.0.3, 8.1.7
    • Component/s: Callbacks
    • None
    • None
    • Fully Compatible
    • Ruby Drivers
    • Not Needed
    • Hide

      1. What would you like to communicate to the user about this feature?
      2. Would you like the user to see examples of the syntax and/or executable code and its output?
      3. Which versions of the driver/connector does this apply to?

      Show
      1. What would you like to communicate to the user about this feature? 2. Would you like the user to see examples of the syntax and/or executable code and its output? 3. Which versions of the driver/connector does this apply to?
    • None
    • None
    • None
    • None
    • None
    • None

      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.

            Assignee:
            jamis.buck@mongodb.com Jamis Buck
            Reporter:
            jamis.buck@mongodb.com Jamis Buck
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved:
              None
              None
              None
              None