Using the RESTAdapter without transactions - is this a terrible idea?


#1

Most of the issues I’ve had with Ember Data stem from trying to manage what gets saved when by moving objects in and out of transactions. I keep thinking I’ve solved the problem in a deterministic way, but then I click around some more and eventually get a “models can’t be in more than one transaction” or “these models aren’t in the same transaction” or similar error.

The BasicAdapter doesn’t use transactions - you just call save on a model instead of commit on a transaction. It’s relatively easy to make this work with the RESTAdapter too - the only thing you need to do is ensure that the willCommit event is triggered on each record before it is saved, so that it is in the right state when didCommit is called after the response returns.

Here’s how I did that:

SaveRESTAdapter = DS.RESTAdapter.extend({
  commit: function(store, commitDetails) {
    var bucket, records;

    for (bucket in commitDetails) {
      records = commitDetails[bucket];
      records.forEach(function(record) {
        return record.send('willCommit');
      });
    }
    return this.save(store, commitDetails);
  }
});

This seems to work well. I can call model.save() to trigger a server request for that particular model and nothing else. I can also call model.rollback() to roll back changes on that model without affecting anything else.

One obvious caveat is that relationship changes that affect multiple models would require multiple save calls. I never actually run into this situation in my app (and I’d be worried if I did, because what if one of those saves failed?), but it’s something to be aware of. Because of the way that the store schedules multiple save calls and flushes them all at the end of the run loop, the effect would be the same, practically, as committing a transaction.

A second caveat is that relationship changes are getting stored on the store’s defaultTransaction and will never be GC-ed. I don’t know if this is really worth worrying about - there are no obvious performance implications in my (limited) testing.

Am I missing any less obvious side effects of overriding this behaviour? The Ember Data codebase is a complex beast and frankly it scares me a bit. I’d appreciate if anyone has any thoughts.

So far this (one, weird) trick has dramatically improved my app’s stability and my productivity / sanity.


#2

@nragaz That’s really helpful, thanks for sharing this (and your blog post)! I was pulling my hair with this issue:

but your solution provides a good alternative.

Just two things:

  • just in case someone else can’t make it work: I believe you need one of the latest ember-data builds (I was on an older one and several functions were missing)

  • there seems to be an issue when relationships are involved. If a newly created record is associated to the one you are trying to save, you will get this error:

    Attempted to handle event willCommit on App.User:ember415:null while in state rootState.loaded.created.inFlight.

In addition, inyour forEach loop, a record might be a relationship and I think they need to be treated in a specific way (they don’t have a “get” method).

So I ended up modifying it a bit. To be honest I am not totally sure about this code (cf the try/catch: I don’t know how to tell if a record is a relationship vs a model, and more generally I find the “state machine” thing difficult to grasp when relationships are involved) and not thoroughly tested, but it works here:

commit: function(store, commitDetails) {
    var bucket, records;

    for (bucket in commitDetails) {
      records = commitDetails[bucket];
      records.forEach(function(record) {
		try {
			if (!record.get('isSaving')){
	        	return record.send('willCommit');
			}
		}
		catch(err){}
      });
    }
    return this.save(store, commitDetails);
}

#3

I should have added:

One obvious caveat is that relationship changes that affect multiple models would require multiple save calls.

I don’t run into this either since I side-load what I need in the serializer. But that might be why I hit the relationship bug I spoke about earlier.


#4

@thingista Thanks for pointing out that you need to be using master for this. I haven’t seen the issue you described where a relationship object is part of the commit details. The issue with records potentially being in a saving state is a tricky one. I usually handle it by disabling the save button while isSaving is true. Ultimately, the state manager still adds a lot of complexity to managing the persistence lifecycle.