Ember Data: A Comprehensive Tutorial for the ember-data Library

Ember Data (a.k.a ember-data or ember.data) is a library for robustly managing model data in Ember.js applications. The developers of Ember Data state that it is designed to be agnostic to the underlying persistence mechanism, so it works just as well with JSON APIs over HTTP as it does with streaming WebSockets or local IndexedDB storage. It provides many of the facilities you’d find in server-side object relational mappings (ORMs) like ActiveRecord, but is designed specifically for the unique environment of JavaScript in the browser.

While Ember Data may take some time to grok, once you’ve done so, you will likely find it to have been well worth the investment. It will ultimately make development, enhancement, and maintenance of your system that much easier.

When an API is represented using Ember Data models, adapters and serializers, each association simply becomes a field name. This encapsulates the internal details of each association, thereby insulating the rest of your code from changes to the associations themselves. The rest of your code won’t care, for example, if a particular association is polymorphic or is the result of a map of many associations.

Moreover, your code base is largely insulated from backend changes, even if they are significant, since all your code base expects is fields and functions on models, not a JSON or XML or YAML representation of the model.

In this tutorial, we’ll introduce the most salient features of Ember Data and demonstrate how it helps minimize code churn, through a focus on a real world example.

An appendix is also provided that discusses a number more advanced Ember Data topics and examples.

The Ember Data Value Proposition

Say we have a working code base for a basic blog system. The system contains Posts and Tags, which have a many-to-many relationship with one another.

All is fine until we get a requirement to support Pages. The requirement also states that, since it’s possible to tag a Page in WordPress, we should be able to do so as well.

So now, Tags will no longer apply only to Posts, they may also apply to Pages. As a result, our simple association between Tags and Posts will no longer be adequate. Instead, we’ll need a many-to-many one-sided polymorphic relationship, such as the following:

Each Post is a Taggable and has many Tags

Each Page is a Taggable and has many Tags

Each Tag has many polymorphic Taggables

Transitioning to this new, more complex, set of associations is likely to have significant ramifications throughout our code, resulting in lots of churn. Since we have no idea how to serialize a polymorphic association to JSON, we’ll probably just create more API endpoints like GET /posts/:id/tags and GET /pages/:id/tags. And then, we’ll throw away all of our existing JSON parser functions and write new ones for the new resources added. Ugh. Tedious and painful.

Now let’s consider how we would approach this using Ember Data.

In Ember Data, accommodating this modified set of associations would simply involve moving from:

The resulting churn in the rest of our code would be minimal and we’d be able to reuse most of our templates. Note in particular that the tags association name on Post remains unchanged. In addition, the rest of our code base relies only on the existence of the tags association, and is oblivious to its details.

An Ember Data Primer

Routes and Models

In Ember.js, the router is responsible for displaying templates, loading data, and otherwise setting up application state. The router matches the current URL to the routes that you’ve defined, so a Route is responsible for specifying the model that a template is to display (Ember expects this model to be subclass of Ember.Object):

Ember Data provides DS.Model which is a subclass of Ember.Object and adds capabilities like saving or updating a single record or multiple records for convenience.

To create a new Model, we create a subclass of DS.Model (e.g., App.User = DS.Model.extend({})).

Ember Data expects a well-defined, intuitive JSON structure from the server and serializes newly created records to same structured JSON.

Ember Data also provides a suite of array classes like DS.RecordArray for working with Models. These have responsibilities such as handling one-to-many or many-to-many relationships, handling asynchronous retrieval of data, and so on.

Note though that, by default, RESTSerializer, willnotaddDS.hasManyassociated IDs to the objects that it serializes, since those associations are specified on the “many” side (i.e., those which have aDS.belongsToassociation). So, in our example, although a Post has many comments, those IDs willnotbe added to the Post object:

A Real World Example: Enhancing an Existing Ordering System

In our existing ordering system, each User has many Orders and each Order has many Items. Our system has multiple Providers (i.e., vendors) from whom products can be ordered, but each order can only contain items from a single provider.

New requirement #1:Enable a single order to include items from multiple providers.

In the existing system, there is a one-to-many relationship between Providers and Orders. Once we extend an order to include items from multiple providers, though, this simple relationship will no longer be adequate.

Specifically, if a provider is associated with an entire order, in the enhanced system that order may very well include items ordered from other providers as well. There needs to be a way, therefore, of indicating which portion of each order is relevant to each provider. Moreover, when a provider accesses their orders, they should only have visibility into the items ordered from them, not any other items that the customer may have ordered from other providers.

One approach could be to introduce two new many-to-many associations; one between Order and Item and another between Order and Provider.

However, to keep things simpler, we introduce a new construct into the data model which we refer to as a “ProviderOrder”.

Drafting Relationships

The enhanced data model will need to accommodate the following associations:

One-to-many relationship between Users and Orders (each User may be associated with0 to nOrders)anda One-to-many relationship between Users and Providers (each User may be associated with0 to nProviders)

Here’s where Ember Data’s support forpolymorphic relationshipswill come in handy. Ember Data supports one-to-one, one-to-many, and many-to-many polymorphic relationships. This is done simply by adding the attributepolymorphic: trueto the specification of the association. For example:

Note that this does not introduceanycode churn; all associations simply work just the way they did prior to this change. The power of Ember Data at its best!

Can Ember Data really model all of my data?

There are exceptions of course, but I consider ActiveRecord conventions as a standard and flexible way of structuring and modeling data, so let me show you how ActiveRecord conventions map to Ember Data:

This will consult a pivotmodelcalled Ownership to find associated Users. If the pivotmodelis basically a pivottable, you can avoid creating an intermediate model in Ember Data and represent the relationship withDS.hasManyon both sides.

However if you need that pivot relationship inside your front-end, set up an Ownership model that includesDS.belongsTo('user', {async: true})andDS.belongsTo('provider', {async: true}), and then add a property on both Users and Providers that maps through to the association using Ownership; e.g.:

This is a many (polymorphic) to many (normal non-polymorphic) relationship. In Ember Data we can express this with a polymorphicDS.hasMany('locatable', {polymorphic: true, async: true})and a staticDS.hasMany('location', {async: true}):

I personally only use sockets for events with very small payloads. A typical event is ‘recordUpdated’ with payload of{"type": "shop", "id": "14"}and then in ApplicationRoute I’ll check if that record is in local cache (store) and if it is I’ll just refetch it.

This way we can send record updated events to all clients without unacceptable overhead.

There are essentially two approaches in Ember Data for dealing with realtime data:

Write an Adapter for your realtime communication channel and use it instead of RESTAdapter.

Push records to the main store whenever they’re available.

The downside with the first option is that it’s somewhat akin to reinventing the wheel. For the second option, we need to access the main store, which is available on all routes asroute#store.

Wrap-up

In this article, we’ve introduced you to Ember Data’s key constructs and paradigms, demonstrating the value it can provide to you as a developer. Ember Data provides a more flexible and streamlined development workflow, minimizing code churn in response to what would otherwise be high impact changes.

The upfront investment (time and learning curve) that you make in using Ember Data for your project will undoubtedly prove worthwhile as your system inevitably evolves and needs to be extended, modified, and enhanced.

APPENDIX: Advanced Ember Data Topics

This appendix introduces a number of more advanced Ember Data topics including:

Keep in mind that you still need to represent the association in the data; links just suggest a new HTTP request and won’t affect associations.

Active Model Serializer and Adapter

Arguably,ActiveModelSerializerandActiveModelAdapterare more used in practice thanRESTSerializerandRESTAdapter. In particular, when the backend uses Ruby on Rails and theActiveModel::Serializersgem, the best option is to useActiveModelSerializerandActiveModelAdapter, since they supportActiveModel::Serializersout of the box.

Embedded Records Mixin

DS.EmbeddedRecordsMixinis an extension forDS.ActiveModelSerializerwhich allows configuring of how associations get serialized or deserialized. Although not yet complete (especially with regard to polymorphic associations), it is intriguing nonetheless.

You can choose:

Not to serialize or deserialize associations.

To serialize or deserialize associations with id or ids.

To serialize or deserialize associations with embedded models.

This is particularly useful in one-to-many relationships where, by default,DS.hasManyassociated IDs are not added to the objects that are serialized. Take a shopping cart that has many items as an example. In this example, Cart is being created while Items are known. However, when you’re saving the Cart, Ember Data won’t automatically put the IDs of associated Items on the request payload.

UsingDS.EmbeddedRecordsMixin, however, it is possible to tell Ember Data to serialize the item IDs on Cart as follows:

As shown in the above example, the EmbeddedRecordsMixin allows for explicit specification of which associations to serialize and/or deserialize via theattrsobject. Valid values forserializeanddeserializeare: -'no': do not include association in serialized/deserialized data -'id'or'ids': include associated ID(s) in serialized/deserialized data -'records’: include actual properties (i.e., record field values) as an array in serialized/deserialized data

Association Modifiers (Async, Inverse, and Polymorphic)

In a polymorphic association, one or both sides of the association represent a class of objects, rather than a specific object.

Recall ourearlier exampleof a blog where we needed to support the ability to tag both posts and pages. To support this, we had arrived at the following model:

Each Post is a Taggable and has many Tags

Each Page is a Taggable and has many Tags

Each Tag has many polymorphic Taggables

Following that model, apolymorphicmodifier can be used to declare that Tags are related to any type of “Taggable” (which may either be a Post or a Page), as follows:

// A Taggable is something that can be tagged (i.e., that has tags)App.Taggable = DS.Model.extend({ tags: DS.hasMany('tag')});// A Page is a type of TaggableApp.Page = App.Taggable.extend({});// A Post is a type of TaggableApp.Post = App.Taggable.extendApp.Tag = DS.Model.extend({ // the "other side" of this association (i.e., the 'taggable') is polymorphic taggable: DS.belongsTo('taggable', {polymorphic: true})});

Inverse Modifier

Usually associations are bidirectional. For example, “Post has many Comments” would be one direction of an association, while “Comment belongs to a Post” would be the other (i.e., “inverse”) direction of that association.

In cases where there is no ambiguity in the association, only one direction needs to be specified since Ember Data can deduce the inverse portion of the association.

However, in cases where objects in your model have multiple associations with one another, Ember Data cannot derive the inverse of each association automatically and it therefore needs to be specified using theinversmodifier.

When data needs to be retrieved based on relevant associations, that associated data may or may not already have been loaded. If not, a synchronous association will throw an error since the associated data has not been loaded.

{async: true}indicates that the request for the associated data should be handled asynchronously. The request therefore immediately returns a promise and the supplied callback is invoked once the associated data has been retrieved and is available.

Whenasyncisfalse, getting associated objects would be done as follows: