mongoid-history icon indicating copy to clipboard operation
mongoid-history copied to clipboard

Changes to embedded documents not tracked in parent history

Open mpetazzoni opened this issue 7 years ago • 10 comments

I'm trying to track all changes to a model object and its embedded documents, as described in the "Include embedded objects attributes in parent audit" section of the README.

My models roughly looks like this:

class ParentModel
  include Mongoid::Document
  include Mongoid::Timestamps
  include Mongoid::History::Trackable
  include Mongoid::Userstamp
  mongoid_userstamp user_model: 'User'

  field :foo
  embeds_one :child
  accepts_nested_attributes_for :child

  track_history on: [:fields, :updated_by_id, :embedded_relations],
                modifier_field: :modifier,
                modifier_field_inverse_of: :nil,
                version_field: :version,
                track_create: true,
                track_update: true,
                track_destroy: false
end

class ChildModel
  include Mongoid::Document
  include Mongoid::Timestamps
  include Mongoid::Userstamp
  mongoid_userstamp user_model: 'User'

  field :bar
  embedded_in :parent, inverse_of: :child
end

Changes to fields of the parent are correctly tracked in the parent's history, but not changes to fields of the child.

> p = Parent.all().first
> p.name = "test"
> p.save
> p.history_tracks.reverse.first
### shows the change to the name in the parent document

> p.child.bar = "test"
> p.save
> p.history_tracks.reverse.first
### no additional audit trail entry recorded, this shows the change to the name of the parent from above

Am I misunderstanding how the tracking of embedded relations is supposed to work? Does it only track a change to the reference itself, for example if I change p.child to a completely different child object?

Is there a way for me to accomplish what I'm looking for here, that is to have a single audit trail of parent document that shows all changes to all its fields, and all the fields of its embedded relations?

I was expecting #150 to provide this functionality.

mpetazzoni avatar Apr 07 '17 17:04 mpetazzoni

The changes with embedded_relations should have worked.

First I would change save to save! and see whether there're errors. Then I would look at what's actually being written in MongoDB with logger at DEBUG level and make sure it's doing what we think it should be doing.

Finally, try to build a short repro in a spec. One thing that comes to mind - is ChildModel declared after or before?

dblock avatar Apr 07 '17 21:04 dblock

I tried with save! as well, no errors. The child object is correctly saved in Mongo, and I can even reload the Parent object (with relations) and get the updated values as expected. It's just the audit trail that's incomplete.

In my application, ParentModel and ChildModel are declared into two distinct files (as you might expect in a Rails app). I don't know in what order those are loaded. From trying to write a spec for it, it does appear that I need to declare the ChildModel before. I'm sending a PR with the spec.

mpetazzoni avatar Apr 07 '17 22:04 mpetazzoni

@mpetazzoni i tried to replicate the behavior by setting up a minimal example. mongoid-history was not able to catch the changes in embedded documents. It basically works on the parent_object.changes hash and if an embedded object attribute is changed, the changes hash remains empty (that is the default behavior of mongoid). So, no history record is not created in that case (which is a bug in mongoid-history).

There is a gem mongoid_relations_dirty_tracking to see the embedded documents changes in parent object's changes hash. I tried to use that in my example, but it turns out the gem does not support rails 5 and mongoid 6. If you are using a lower version of rails and mongoid, you can give it a try.

Another solution on SO.

It might take some more time to try these solutions on my end. I will let you know if i find a fix. Please let us know, if any of the above solutions work for you.

jagdeepsingh avatar Apr 11 '17 05:04 jagdeepsingh

I have patched for support of Rails5/Mongoid6 mongoid_relations_dirty_tracking here. Problem is that test is still failing even when examples from mongoid_relations_dirty_tracking are fine.

bopm avatar Apr 13 '17 14:04 bopm

Any news about it? How you solved it?

rubydev avatar Oct 30 '18 09:10 rubydev

https://github.com/mongoid/mongoid-history/pull/191 seems like it's almost there, but not ready to merge

dblock avatar Oct 30 '18 13:10 dblock

How it should work actually? For example in case embeds_many, that parent is Post and children are comments. And one comment was changed, should be in original and modified all comments? Or changed comment only?

Edit: After investigation, I think, that there should be all embedded objects, not changed only.

rubydev avatar Oct 30 '18 16:10 rubydev

My temporary solution is:

# app/models/concerns/mongoid/embedded_dirty_tracking.rb:

module Mongoid
  module EmbeddedDirtyTracking
    extend ActiveSupport::Concern

    included do
      after_initialize :store_embedded_shadow
      after_save :store_embedded_shadow
    end

    def store_embedded_shadow
      @embedded_shadow = {}

      self.class.tracked_embedded.each do |rel_name|
        @embedded_shadow[rel_name] = tracked_embedded_attributes(rel_name).dup
      end
    end

    def embedded_changes
      changes = {}

      @embedded_shadow.each do |rel_name, shadow_values|
        current_values = tracked_embedded_attributes(rel_name)
        if current_values != shadow_values
          changes[rel_name] = [shadow_values, current_values]
        end
      end
      changes
    end

    def embedded_changed?
      !embedded_changes.empty?
    end

    def changed_with_embedded?
      changed? or embedded_changed?
    end

    def changes_with_embedded
      (changes || {}).merge(embedded_changes)
    end

    def tracked_embedded_attributes(rel_name)
      values = nil
      case relations[rel_name].macro
      when :embeds_one
        values = send(rel_name)&.attributes&.clone || {}
      when :embeds_many
        values = Array.new
        values += send(rel_name).map { |child| child.attributes.clone }
      end
      values
    end

    module ClassMethods
      def tracked_embedded
        relations.select {|_, rel| %i[embeds_one embeds_many].include?(rel.macro) }.keys
      end
    end
  end
end
# app/models/concerns/trackable.rb

module Trackable
  extend ActiveSupport::Concern

  included do
    include Mongoid::EmbeddedDirtyTracking
    include Mongoid::History::Trackable

    track_history on: [:fields, :embedded_relations], except: [:_keywords], changes_method: :changes_with_embedded
  end
end
# app/models/city.rb

class City
  include Mongoid::Document
  include Trackable

  field :shortcut, type: String
  field :name, type: String

  embeds_many :excursions, cascade_callbacks: true
  accepts_nested_attributes_for :excursions, allow_destroy: true
end
# app/models/excursion.rb

class Excursion
  include Mongoid::Document

  field :name, type: String

  embedded_in :city
end

Inspired from https://github.com/Polarion/mongoid_relations_dirty_tracking

rubydev avatar Nov 02 '18 12:11 rubydev

Partial fix in https://github.com/mongoid/mongoid-history/pull/232, please try HEAD!

dblock avatar Jun 11 '19 02:06 dblock

No joy with HEAD. This seems an issue for mongoid - Dirty should track changes when embedded documents are changed via the <relation>_attributes= path. I have not been able to find anything about this in the Mongoid JIRA issues history yet.

Failing example:

parent.posts_attributes= {"0": {subject: "test"}}
parent.save
parent.changes 
=> {}

the result from parent.changes should include the embedded document changes. Created this issue in Mongoid's JIRA: https://jira.mongodb.org/browse/MONGOID-4896

vanboom avatar Jun 11 '20 16:06 vanboom