Successful single page applications must overcome a number of hurdles, and large among them is the persistence of data. It is, however, not the only challenge. To keep Ember focused and attractive to a wide audience (with very different persistence requirements), the core team long avoided declaring any one data layer "official". In the fall of 2013 that changed, and Ember-Data was declared an official, use-it-in-production persistence library.
Because of the long gap between Ember being production-ready and Ember-Data catching up, several other Ember persistence libraries have emerged. Among the other options are:
Beyond this list, there are many Ember projects that forgo a persistence library completely and roll their own data layer. Most famously Discourse, the popular open-source web forum software.
Any JavaScript implementation of data persistence faces similar challenges:
In detail, this chapter will talk about those challenges and how Ember-Data approaches them. The goal is to give you a solid foundation for working with any persistence layer you choose, as well as give you a taste of the approaches chosen by Ember-Data.
Before getting down to details, a glance at how Ember-Data is used in the context of a simple application is useful. First, an exceedingly trivial example:
// Presuming Ember and Ember-Data are loaded. var App = Ember.Application.create(); App.User = DS.Model.extend({ name: DS.attr('string') }); App.Router.map(function(){ this.resource('user', {path: '/user/:user_id'}); }); Ember.TEMPLATES['user'] = Ember.Handlebars.compile("{{user.name}}");
When routing to /user/42, this example will query a server for user 42 and display her name. The :user_id fragment in the resource's path hints to Ember that the User model is being requested. This allows us to avoid explicitly creating a route and model hook.
Like most of Ember, Ember-Data is heavily driven by convention over configuration. In practice, using Ember-Data is fairly configuration-free. However, API and persistence implementations do vary wildly. An understanding of how Ember-Data approaches the challenges of persistence internally is the cornerstone to using the tool successfully. Even if you don't need to modify any of these internals for your first project, it is probably inevitable you will at some point.
After reading through this chapter, come back to the above example and think about what happens behind the scenes.
Almost any persistence layer used in the browser will be asynchronous. Synchronous APIs like HTML5 localStorage are the exception, and AJAX is usually the rule. How libraries choose to approach asynchronous behavior largely defines their own API and feature set.
In modern JavaScript, there are three common approaches:
Promises have become somewhat universal for managing asynchronicity in Ember, so Ember-Data (and indeed, all the other libraries mentioned) embrace promises. For instance, to find a record in Ember-Data:
store.find('user', 1) .then(function(user){ // Yes we have a user! }, function(error){ // 404? 500? Some kind of error. });
When you use promises in Ember (unlike with other asynchronous handlers), you rarely need to think about the Ember run-loop. When the promise is resolved, it has already been put into a loop (if you plan to author a library, that is another thing entirely).
Ember uses RSVP to implement promises, and all the aforementioned persistence layers do the same. One reason for this is RSVP's configurability- when used via Ember, RSVP schedules certain asynchronous actions into the Ember run loop for a performance improvement.
The Ember Router is also promise-aware, as discussed in chapter 8. In this way, Ember-Data (and other libraries) work smoothly with the loading states provided by the router:
App.UsersIndexRoute = Ember.Router.extend({ model: function(){ return this.get('store').findAll('user'); } });
This route will wait for model data to return before proceeding to render its template.
Promises are a powerful and very flexible way to manage asynchronous behavior. They can be chained, behave consistently in synchronous and asynchronous situations, and have a powerful (if not slightly confusing) way to handle errors.
In browser environments, the roundtrip to fetch a persisted item is often very latent. Fetching a single record may take 60 or 70 milliseconds in a best case, with additional delays parsing the data in JavaScript. Often, the times are far worse than this.
This overhead (among other concerns) makes a local, in-memory record cache imperative. In Ember, we call these record caches "stores". Not all of the libraries listed earlier support something like this. The store provided by Ember-Data is a bit more than just a cache, it also provides the primary interface used for working with the library.
Ember-Data uses dependency injection to place the store on all controller and route instances. For example, createRecord can be used in a route to generate a new user for a sign-up form:
var App = Ember.Application.create(); App.User = DS.Model.extend({ username: DS.attr('string'), password: DS.attr('string') }); App.Route.map(function(){ this.route('sign_up'); this.route('dashboard'); }); App.SignupRoute = Ember.Route.extend({ model: function(){ return this.get('store').createRecord('user'); }, actions: { save: function(user){ var route = this; user.save().then(function(){ route.transitionTo('dashbaord'); }) } } }); // Presume some templates.
When debugging, or for certain examples, you can also find the store on Ember's container:
var store = App.__container__.lookup('store:main');
But this is not a stable, public API for using Ember-Data, and you should instead look to the "store" property on controllers and routes.
When requesting a record already in the store, that record will be returned from memory. Records may also be returned and then updated once a request to the persistence endpoint has completed. Most methods on the store return promises, others return live arrays of data.
"Live" arrays and Ember-Data's server interactions have been intentionally brushed over here. In normal usage of Ember-Data, most code is written against the store or record APIs. Understanding a little of the internals will help you use them effectively and expertly.
Persistence can be performed in many ways. An Ember application may use the HTML5 localStorage or IndexedDB APIs, an HTTP API, a websocket connection, or any of a number of other methods. During test execution, faking persistence may be desired. In order for a persistence library to support all of this, it must have an appropriate layer of abstraction. We call this layer an adapter.
Adapters are only responsible for interacting with the low-level persistence endpoint (such as AJAX). They are not responsible for caching records in memory or maintaining any state at all. They are not (directly) responsible for converting a model into data for an endpoint, only for submitting it to the endpoint
There are three adapters common to Ember persistence libraries:
In Ember-Data, you can configure a default adapter at several levels. The simplest being to name your custom adapter appropriately:
var App = Ember.Application.create(); // Use an optionally extended copy of the in-memory FixtureAdapter. App.ApplicationAdapter = DS.FixtureAdapter.extend();
You can also configure an adapter for a specific model, allowing different models to persist in different ways but still have relationships and share an in-memory cache.
Adapters in Ember-Data implement several hooks:
All of these hooks (besides generateIdForRecord) are expected to return promises. In this way, a synchronous persistence adapter such as localStorage has the same interface as an asynchronous one like AJAX.
The localStorage adapter is a simple and useful demonstration of a custom adapter.
Before data can go to an endpoint, it often requires some conversion. For instance, given a model attribute containing a date, it must first be made a string or integer to be sent as JSON over AJAX. Serializers are responsible for this conversion of attributes and relationships. In turn, they are also responsible for the deserialization of endpoint data into a normalized format for model instantiation.
As mentioned above, adapters are only responsible for submitting and retrieving data from an endpoint. In Ember-Data, they are truthfully also responsible for converting models into data for that endpoint, but if an adapter is implemented as an extension of DS.Adapter (and almost every adapter should be), then Ember-Data provides for custom serializers.
Again, the simplest way to change the default serializer is with appropriate naming:
var App = Ember.Application.create(); // Use an optionally extended copy of the REST serializer. App.ApplicationSerializer = DS.RESTSerializer.extend();
Ember-Data ships with DS.JSONSerializer, a rather minimal converter between models and JSON, and with DS.RESTSerializer, a serializer mapping objects in a more JSON-API compatible way. For customization, the REST serializer is a likely a better starting point.
The serializer is responsible for transforming a whole object, but also provides an extensible system for transforming attributes. Here is a demonstration:
// Presumes Ember, Ember-Data, and moment.js var App = Ember.Application.create(); App.User = DS.Model.extend({ name: DS.attr('string'), signedInAt: DS.attr('moment') }); App.MomentTransform = DS.Transform.extend({ serialize: function(value) { if (value) { return value.format('YYYY-MM-DDTHH:MM:SSZ'); } else { return value; } }, deserialize: function(value) { if (moment) { return moment(value); } else { return value; } } });
This serializer creates a new Ember-Data attribute type of "moment", which
will serialize and de-serialize to timestamps with timezone information that can be parsed on the server.
Creating relationships between models poses several challenges, especially in Ember where data-binding means any list that should contain a new model should instantly be updated with it:
These are not small challenges. Creating a relationship in Ember-Data is similar to creating attributes on a model:
var App = Ember.Application.create(); App.User = DS.Model.extend({ tickets: DS.hasMany('ticket') }); App.Ticket = DS.Model.extend({ user: DS.belongsTo('user') });
Only hasMany and belongsTo relationships exist. They can be polymorphic, and specify special inverse naming. Most notable, they can be asynchronous or synchronous. For example, given the previous example:
// Use the fixture adapter App.ApplicationAdapter = DS.FixtureAdapter; // Normally the store is available on controllers and routes, // here we must fetch it. var store = App.__container__.lookup('store:main'); var user = store.createRecord('user'); var userTickets = user.get('tickets'); userTickets.get('length'); // => 0 var ticket = store.createRecord('ticket', {user: user}); userTickets.get('length'); // => 1
Fetching a relationship with get returns a live array, a set of objects that updates itself based on whatever is in the store (persisted or unpersisted). Alternatively, the relationships can be defined as asynchronous:
var App = Ember.Application.create(); App.ApplicationAdapter = DS.FixtureAdapter; var store = App.__container__.lookup('store:main'); App.User = DS.Model.extend({ tickets: DS.hasMany('ticket', {async: true}) }); App.Ticket = DS.Model.extend({ user: DS.belongsTo('user') }); var user = store.createRecord('user'), userTickets; // The tickets relationship is async, so a promise // is returned instead of a live array. user.get('tickets').then(function(tickets){ userTickets = tickets; }); userTickets.get('length'); // => 0 var ticket = store.createRecord('ticket', {user: user}); // The promise returned a normal array of objects, not // a live array. The length will not change despite the new // object added to the store. userTickets.get('length'); // => 0
Ember-Data's relationship code has been through several painful refactors, but at this writing has seemingly stabilized in to quite a mature and capable solution. Where there are tradeoffs to be made, it at least offers you clear choices.
Model resolution, how a model used in a relationship or passed to the store, is handled differently in each of the libraries listed above. Model resolution is basically a dependency problem, and therefore (in JavaScript) a namespace and load-order challenge. Ember-Data takes advantage of an Ember feature designed to manage just this set of issues: Containers.
In the prior section on relationships, we related two models with a string. To bind the tickets of a user to the ticket model, the string "tickets" was passed to DS.hasMany.
This exposes a little of Ember's container usage. Instead of hardcoding the fetching of models, the store will look to the application's container to lookup models.
// …snip App.Ticket.extend({ user: DS.belongsTo('user') });
The "user" string is used (internally) to create a call to the container:
userClass = container.lookupFactory('models:user');
And thus models flow through the same loading and dependency injection framework used by controllers, routes, views, and pretty much everything else in Ember. This is an elegant and powerful solution.
Take a look back at the first "at a glance" example at the start of this chapter. Impressive that so many abstractions are made so simple!
Ember-Data's terse syntax for basic usage and clean abstractions make it a quick and flexible platform for app development. For testing applications, the fixture adapter's simple API and ability to emulate asynchronous backends makes it a popular choice. An application can even be prototyped using the fixture adapter, then moved over all at once or model-by-model to a REST adapter for live data. Customizable serializers and adapters can make legacy APIs act like modern ones in application code.
In addition to Ember-Data's own built-in adapters, there are a plethora of other adapters provided by the community and by API authors.
Persistence in the browser is a complicated challenge. In the next four chapters we will enter the practical phase of this book, and we will see how Ember-Data manages that complexity.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.