CodeBrief

10 Things You Can Do With EPF (That You Can't Easily Do With Ember Data)

There exists a viewpoint that Ember Data is "broken"; that the core team has abandoned the project and that the codebase is awash with issues. This is far from the reality of the situation, as I personally know many people who are successfully using ED on real projects. It is also my understanding that ED is not abandoned and that the core team is fostering a renewed interest in pushing it forward. The truth is more complex and ties into why we created Ember.js Persistence Foundation.

One way to shed light onto the situation is to give a simple enumeration of things that ED lacks and EPF provides. Having used ED for several years on a large project, these are things that, in our case, are major issues, but are perhaps surmountable for others. These are also issues that, based on my understanding of how ED is structured, are going to be fairly non-trivial to properly implement.

1. Create a Parent-Child Hierarchy

Ember Data has the notion of client-side relationships. It also has the notion of transactions which can be used to group operations. Reconciling these two features together, you might think you could do something like the following:

var post = transaction.createRecord(App.Post, {title: 'parent'});
var comment = transaction.createRecord(App.Comment, {title: 'child'});
post.get('comments').pushObject(comment);
transaction.commit();

Unfortunately, the above code will generate an error. The reason for this is that ED does not order operations around dependencies. Both requests will be sent at the same time and the child will not have the parent's id yet. A workaround for this is to not use the same transaction and instead save the parent and child separately. In practice, this constraint puts a huge limitation on transactions and forces you to fully understand what the transaction is doing before committing.

In EPF, all operations are topologically sorted around dependencies:

// epf has the notion of a `session` instead of a `transaction`
var post = session.create('post', {title: 'parent'});
var comment = session.create('comment', {title: 'child'});
post.get('comments').pushObject(comment);
session.flush();

The above code will function correctly. The requests will be ordered such that the request to save the comment will wait for the request to save the post to complete. In fact, EPF supports arbitrarily complex operations with any number of dependencies.

As a side note, in the past I have submitted a pull request to give ED this functionality, but the situation has since become more complex and some conflicting features having been added.

2. Know When a Transaction Completes

Simply put, there is currently no good way in ED to know when a transaction has completed. transaction.commit does not return a promise and there are no events on the transaction to listen to.

In effect, this means that you must listen to the lifecycle events on the individual models inside the transaction to get some insight as to the state of the transaction:

transaction.commmit();
post.one('didCreate', function() {
  // all we know for certain here is that the
  // post was created, in order to know the transaction
  // has finished we must listen to this on all models
});

In EPF, the situation is much simpler. session.flush (the EPF equivalent of transaction.commit()) returns a promise. To handle a successful transaction, simply use the normal promise callbacks:

session.flush().then(function() {
  // the session has fully flushed here
}, function() {
  // this will be hit if the something went wrong
});

3. Change Models While Being Saved

Every single model in Ember Data is backed by a state machine. Initially, models will be in a clean state. Modifying any of the attributes of the model will move it to a dirty state. Committing the model will move it into an inFlight state.

The issue here, is that this implementation causes models to "lock". While the model is in the inFlight state, any attempt to modify any of its attributes will throw an error:

post.set('title', 'new title');
transaction.commit();
// this will throw an error since the request has not finished
post.set('title', 'a newer title');

Thus applications must be designed around this constraint: spinners everywhere.

In my opinion, it is a perfectly reasonable application design to optimistically assume that some requests finish successfully. Alex MacCaw talks about this in his post about Asynchronous UIs.

This is fully supported in EPF. A model can be modified at any time:

post.set('title', 'new title');
session.flush();
// no problems here
post.set('title', 'a newer title');
// you can even flush again before the previous flush completes
session.flush();

4. Modify a Model in Isolation

One of Ember.js's virtues is that it encourages a central source of truth. Changing an attribute of a model will automatically update the UI everywhere.

This is usually desirable, but not always. Sometimes you want to be able to modify a model and not have the UI update until the user has indicated that the updates have been finished. For example, imagine the user is editing her username, and that username is currently visible in several places. It would be jarring to see these updates happen in real-time all over the place– especially if the action is ultimately cancelled.

In Ember Data, there is no way to "fork" models. Only one instance of a model can logically exist. The workaround is to buffer changes outside the model and copy them over at some later point.

EPF is designed around multiple instances of a model existing. This idea is central to its implementation. When building an application using EPF, a child session can be used to isolate changes:

// calling newSession will create an isolated session
var childSession = session.newSession();
var childPost = childSession.add(post);
// this will not affect the parent session
childPost.set('title', 'new title');
childSession.flush();
// at this point the parent session will have the changes

5. Properly Handle Embedded Records

Both EPF and Ember Data support embedded records. The adapter can be configured to indicate that, instead of sideloading, child models should be directly embedded in the returned json. This also changes the relationship semantics such that child models will be saved in the same request as the parent models.

In Ember Data, lifecycle events are not called correctly on embedded records. Moreover, there is a deep unresolved ambiguity around what actual model instance to use when embedded models are created. For example: (assume that App.Comment is embedded in App.Post for this section)

var comment = transaction.create(App.Comment, {title: 'embedded child'});
post.get('comments').pushObject(comment);
transaction.commit();
post.one('didUpdate', function() {
  var childComment = post.get('comments.firstObject');
  // the below expression *should* evaluate to true, but ED currently
  // creates new instances for newly created embedded children
  comment === childComment; // false
});

Illustrated in the above code, Ember Data always creates new instances of embedded models when they are created. This means that any references to the previous child will be erroneous. This is bad.

The reason for this is that there actually is an ambiguity here. Is the model that the server is returning the same model? It is within the realm of possibility that the server could have acknowledged the request from the client, but thrown out the newly created embedded model and instead returned a different embedded model that was created elsewhere.

EPF solves this by supporting pass-through client ids. All models in EPF have a clientId associated with them. The backend can optionally serialize the clientId back to the client to resolve this ambiguity. Thus, in this case, the following code will work as expected:

var comment = session.create('comment', {title: 'embedded child'});
post.get('comments').pushObject(comment);
session.flush().then(function() {
  var childComment = post.get('comments.firstObject');
  comment === childComent; // true
});

6. Deal With Errors

The elephant in the room when using ED is that errors are barely supported. It is true that if the server returns a 422 response code with a list of field errors, ED will parse that and set the errors property on the model. Things like 404 responses are not supported. This basic scenario is extremely important to many applications and is hard to work around.

The error handling situation in ED becomes even more complex when taken in the context of transactions. Transactions are non-retryable. This means that if part of a transaction creates an error, there is no way to retry the transaction and correct the error. Moreover, after an error, the transaction is lost and must be manually re-created.

EPF isn't perfect in this regard either, but is an improvement. Things like 404's and other errors are easily handled:

session.load('post', 1).then(function() {
  // success
}, function() {
  // possible 404
});

In addition, EPF allows sessions to be flushed multiple times to correct errors. Since flush() calls are also promises, there are clear-cut hooks to handle errors.

session.flush().then(function() {
  // success
}, function() {
  ... // there was an error, do something to correct
  session.flush(); // peform another flush to retry
});

7. Handle "Conflicts"

As previously mentioned above in section 4, ED locks models while they are being saved to the server. Perhaps one reason why it was implemented in such a way was to simplify the realm of possible conflict scenarios. Since the model is locked, there is no chance that it was updated and the latest version of the server can be used directly.

Even with such an implementation, I claim it is impossible to avoid conflict scenarios in any long-running app. Even with locking, there are still numerous error scenarios that would require some sort of conflict resolution. For instance, what if a request fails and the server indicates that it has newer data? What should the client do?

Ember Data provides no conventional way to resolve these cases. EPF is built around these cases.

All new data loaded into a session goes through a session.merge() call. EPF maintains enough versioning information on the client to provide rich merge hooks. It also provides out of the box merging strategies that can get you most of the way.

8. Custom Actions

When designing an API, it is a tall order to enforce strict REST compliance. REST is a good convention to follow, but ultimately it is nice to be able to have custom actions outside of basic CRUD methods.

For instance, one might want to approve a post through a custom endpoint (e.g. /posts/1/approve). When using a non-rest adapter, the need for custom methods (RPCs) is even more apparent.

Currently, Ember Data provides no conventional way to do this. Its conventions are also moving in the direction of JSON API which as far as I know has no specification/proposal to do this.

EPF provides a first-class way to do this with the ability to send arbitrary payloads down to the client:

session.remoteCall(post, 'approve').then(function() {
  // the remote call has completed and possibly loaded new changes
});

9. Load Data in the Background

This point is a direct result of the ED's locking models and inability to deal with conflicts. This is such a common scenario in modern client applications, however, that I think it is necessary to explicitly point out.

Ember Data does support side-loading, but even this is wrought with errors. For instance, if you sideload a model that is currently in the inFlight state, an error will been thrown.

A much richer user experience can be delivered by streaming in updates from the backend. EPF has been designed with this thought in mind. At any point session.merge() can be called to load in new data and the corner cases will be properly handled.

10. Think in Terms of the Container

When building an Ember.js application, you spend less time referencing explicit class names, such as App.Post, and more time simply referencing container names. This doesn't mean that you have to understand the container and how dependency injection works, but it gives you solid conventions to follow. For instance, at the template layer, something like the following is used:

{{linkTo 'post' model}}

Instead of:

{{linkTo 'App.PostRoute' model}}

In ED, however, you still reference class names:

transaction.create('App.Post', {title: 'herp'});

EPF conforms to similar conventions as Ember.js proper and you can simply reference container names:

session.create('post', {title: 'derp'});

Afterword

When evaluating a potential data framework to use with your Ember.js application, I hope that these issues give some clarity as to what EPF can provide over Ember Data. Most of these points apply to changing data, not reading data. For a mostly read-only application, ED will most likely still be sufficient. For applications that are write-heavy, I would recommend EPF. On the other hand, if all you desire is a lightweight wrapper around $.ajax, other solutions such as Ember Model might be sufficient.

comments powered by Disqus