Dirt tracking with Ember-Data - only save dirty properties

Hi @coladarci , generally yes, but there is a main problem. You have to mimic everything that Ember does. Especially Embedding for belongsTo and hasMany. I can show it up, but whenever there is a change in Ember Data you have to adapt your serializer. At the moment Im in going for final examens, but I can show up my solution and the one we use at the company. I think the goal should be to go in depth and make a PR, but for me personally thats not possible before jan/feb. I will clean up my solution and post in the next days and maybe it will help you out. Might be good to know what Version of ED you are using.

Hey @gerrit, that would be amazing. Currently, we are on Ember 1.8.0-beta.1 and Ember Data 1.0.0-beta.9.

As an FYI, the use-case that is failing for me is when you get a validation error back from the server; ember doesn’t treat the un-saved properties as dirty so they never get sent to the server.

We’d love to help with the PR, part of me thinks this is flat-out-broken in that if I have a model, I make a change to an attribute, save but don’t get a 200 back, dirty attributes is empty.

Hi everyone. Here is my solution for the problem for ActiveModelSerializer. It was not tested much yet, but i hope it helps someone. Ths solution tracks relationships as well as regular attributes, and it does not uses much internal api nor re-implement anything. But the downside is that it uses one observer per relationship on every model instance.

DS.Model.reopen
  setupChangeObservers: Em.on 'init', ->
    @_changedRelations = []

    @eachRelationship (name, relationship) =>
      observer = change: => @_changedRelations.push(name)
      @addObserver name, observer, 'change'

  resetChangedRelations: Em.on 'didLoad', 'didUpdate', 'rollback', ->
    @_changedRelations = []

  allChangedKeys: ->
    relations  = @_changedRelations
    attributes = Em.keys @changedAttributes()
    inFlight   = Em.keys @_inFlightAttributes

    [].pushObjects(relations)
      .pushObjects(inFlight)
      .pushObjects(attributes)
      .uniq()

DS.ActiveModelSerializer.reopen
  serializeAttribute: (snapshot, json, key, attribute) ->
    if snapshot.allChangedKeys().contains(key)
      @_super snapshot, json, key, attribute

  serializeHasMany: (snapshot, json, relationship) ->
    key = relationship.key
    if snapshot.allChangedKeys().contains(key)
      @_super snapshot, json, relationship

  serializeBelongsTo: (snapshot, json, relationship) ->
    key = relationship.key
    if snapshot.allChangedKeys().contains(key)
      @_super snapshot, json, relationship

I was thinking, since there is now the new snapshot api, it could be done with a one-liner. Think of it. A snapshot gets newly generated each time you serialize a record. So the simplest thing would be.

serialize: (snapshot, options) ->
  snapshot._attributes = snapshot.record._inFlightAttributes
  @_super(snapshot, options)

And the next the the model gets serialized, theres a new snapshot and the original record isnt touched.

@gerrit, this approach was the first thing i tried, but in my case snapshot.record was null, and relationships were not appearing in _inFlightAttributes

I am using Ember 1.9.1; Ember Data 1.0.0-beta.14.1

UPD: Ok, Ember-data beta 14 is passing records, not snapshots. I have updated to beta 15, but i still need realtionships tracking. Here is my code updated to work with beta 15, plus i’ve made it a bit more compact:

DS.Model.reopen
  _setupChangeObservers: Em.on 'init', ->
    @eachRelationship (name, relationship) =>
      observer = change: => @_changedRelations.push(name)
      @addObserver name, observer, 'change'

  _resetChangedRelations: Em.on 'init', 'didLoad', 'didUpdate', 'rollback', ->
    @_changedRelations = []

  hasKeyChanged: (key)->
    [].pushObjects(@_changedRelations)
      .pushObjects(Em.keys @changedAttributes())
      .pushObjects(Em.keys @_inFlightAttributes)
      .uniq()
      .contains(key)

DS.ActiveModelSerializer.reopen
  serializeAttribute: (snapshot, json, key) ->
    @_super.apply this, arguments if snapshot.record.hasKeyChanged(key)

  serializeHasMany: (snapshot, json, relationship) ->
    key = relationship.key
    @_super.apply this, arguments if snapshot.record.hasKeyChanged(key)

  serializeBelongsTo: (snapshot, json, relationship) ->
    key = relationship.key
    @_super.apply this, arguments if snapshot.record.hasKeyChanged(key)
1 Like

that really looks nice… I remember something that Em.Data Beta15 could track changes in relationships, but I didnt find it yet.

I am not to sure if observer are the best way, still it works and is better than none.

Ok I found another approach with the new snapshot api of Ember Data beta 15.

   serializeAttribute: function(snapshot, json, key, attributes) {
      if ( snapshot.record['_inFlightAttributes'] != null 
           || snapshot.get('isNew') )  {
        return this._super(snapshot, json, key, attributes);
      } else {
        return
      }
    }

The same could be done with relationships I guess via:

http://emberjs.com/api/data/classes/DS.ActiveModelSerializer.html#method_serializeBelongsTo and http://emberjs.com/api/data/classes/DS.ActiveModelSerializer.html#method_serializeBelongsTo

Thanks for your effort @gerrit. Should your second line be like this?

if ( snapshot.record['_inFlightAttributes'][key] != null 

The problem here is that _inFlightAttributes contains only regular attributes, not relationships. In my example above, i am overriding the methods you’ve pointed out to, i am not using _inFlightAttributes explicitly to keep bodies of serialize* methods uniform, but if you look at hasKeyChanged, you will see that this hash is used there, it is just not enough

Here is my revised solution that works with Ember CLI version 0.2.7, Ember version 1.13.0, and Ember Data version 1.13.4

Add the following to your serializer:

serializeAttribute: function(snapshot, json, key, attributes) {
// Check if new record
if (snapshot.get('isNew'))  {
  // Is new
  return this._super(snapshot, json, key, attributes);
}  else {
  // Check if current attribute is in flight
  let attrKey = '_internalModel._inFlightAttributes.'+key;
  if (snapshot.get(attrKey) != null) {
    // Attribute is dirty
    return this._super(snapshot, json, key, attributes);
  } else {
    // Attribute is not dirty, not stored in inFlightAttributes
    // Ignore this attribute
    return;
  }
}

Thanks everyone for your help!

I found a little bit of a cleaner function on the snapshot we can use here, called changedAttributes()

Also does a check to see if the prop is null, and omits it.

serializeAttribute: function(snapshot, json, key, attributes) {
  if ( snapshot.changedAttributes()[key] || snapshot.record.get('isNew'))  {
    return this._super(snapshot, json, key, attributes);

    Object.keys(json).forEach((k) => {
      if (json[k] === null) {
        json[k] = undefined;
      }
    });
  } else {
    return;
  }
}

Also - anyone here have any issues recently with changes to belongsTo properties not recognized by inFlightAttributes?

I’m confused - nothing after the first return will execute… am I taking crazy pills?

Oops! Must have been a typo.

serializeAttribute: function(snapshot,json,key,attributes) {
  if ( snapshot.changedAttributes()[key] || snapshot.record.get('isNew'))  {
    var t = this._super(snapshot, json, key, attributes);

    Object.keys(json).forEach((k) => {
          if (json[k] === null) {
            json[k] = undefined;
          }
        });

        return t;
      } else {
        return;
      }
}
2 Likes

@chrismllr Any idea how to find the changes in association(belongsTo specifically)?

So there is a serializeBelongsTo hook in the RESTSerializer, along with serializeAttribute. It provides you with the same arguments

You can check out all the options here in the ember data docs for RESTSerializer RESTSerializer - 4.6 - Ember API Documentation

1 Like

I am aware of the serializeBelongsTo hook, but DS model’s changedAttributes method doesn’t keep track of belongsTo model changes. Which means there is no way for me to send the belongsTo attributes only when they change, instead I have to send all belongsTo association.

Assume there is a DS model for post like the one below

Post = DS.Model.extend({
  title: DS.attr('string'),
  author: DS.belongsTo('user')
});

Assume that post one has user with id 1 as author. Now if I change author to user with id 2, changedAttributes methods doesn’t contains details of the belongsTo association changed i.e.

   post = Post.find(1);
   post.changedAttributes(); // Object {}
   user = User.find(2);
   post.set('user', user);
   post.changedAttributes(); // still Object {}

P.S: I dont want the post to keep track changes in any of the user attributes, but I want to know changes in relationships if the Post’s author is pointing to new User model.

@chrismllr Any idea how to get it?

So this opens the can of worms, the hasDirtyAttributes property on a model is not triggered by its relationships (hasMany, belongsTo)

I found this RFC -

https://github.com/emberjs/rfcs/pull/21

But ended building out something similar to the first answer in this stack overflow question, another property on the model called isDeepDirty. Its very old, and needs a bit of tinkering. Hope this helps.

Hi all, I use the ember-data-change-tracker addon to accomplish this. Simple install, activate in model and add the keep only changed mixin to the required model serializer, all outgoing requests will now only send attributes that have changed.

1 Like

Or you can add // app/serializers/post.js

export default DS.JSONAPISerializer.extend({

serializeAttribute(snapshot, json, key, attributes) {
if (snapshot.record.get(‘isNew’) || snapshot.changedAttributes()[key]) { this._super(snapshot, json, key, attributes); } }

}); from Saving Only Dirty Attributes - Ember Igniter

BTW Why this is not in Ember ? As I see model.save(‘becomeDirty’) is deprecated ? It takes me ages to realise that.

BTW2 Is there a chance to observe ‘dirtiness’ of object properties ? export default DS.Model.extend({ visitor:DS.attr() ← observ doesn’t work

ember-data-change-tracker => also tracks dirtiness of object properties, which ember data does not do

1 Like