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

Embedded docs - allow option to overwrite with $set rather than $push/$pull

    • Type: Icon: New Feature New Feature
    • Resolution: Unresolved
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: None
    • Component/s: Persistence
    • Labels:
      None
    • Ruby Drivers

      Currently, when updating embedded docs on a model, Mongoid will smartly do a "diff" of existing docs vs. target docs. The docs which were "added" in this diff will be set via the atomic $push operated, while removed docs will be removed with the $pull operator.

      This is in general a very nice way to do things, because it means that multiple users updating the same object at the same time won't overwrite each other's embedded doc edits. It should remain the "default".

      However, it has two drawbacks:

      1. It does not preserve ordering of docs. For example, suppose model X has existing embedded docs [A, B]. If I change its docs of [C, D, A] in Mongoid, then it will do $pull [B] and $push [C, D], leaving the final order as [A, C, D] rather than [C, D, A].
      2. It can lead to duplicate insertion of the same new doc, if you are not using object IDs when inserting.

      Therefore, there should be a way that on a given update operation, you can force the embedded doc field to use a $set rather than a $push/$pull.

      Importantly, I think this option should be a parameter to the `.save!` method, rather than a declarative option inside the model itself, because it is often controller-context dependent on how you save.

      Currently I manually hack this in my code like this:

      class Customer
        include Mongoid::Document
        embeds_many Phone
      
        attr_accessor :force_overwrite_phones
      
        def atomic_updates(_ = false)
          mods = super
          return unless force_overwrite_phones
      
          mods['$push']&.delete('phones')
          mods['$pull']&.delete('phones')
          mods[:conflicts]&.[]('$push')&.delete('phones')
          mods[:conflicts]&.[]('$pull')&.delete('phones')
          mods['$set'] ||= {}
          mods['$set']['phones'] = send(:phones).map(&:attributes)
          mods
        end
      end
      
      bob = Customer.first
      bob.phones += [Phone.new(number: '+12223334444')]
      bob.save! # does not overwrite
      
      bob.phones += [Phone.new(number: '+12225556666')]
      bob.force_overwrite_phones = true
      bob.save! # does overwrite

            Assignee:
            Unassigned Unassigned
            Reporter:
            shields@tablecheck.com Johnny Shields
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: