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.

Note: This article presumes some basic familiarity with Ember.js. If you aren’t familiar with Ember.js, check out our popular Ember.js tutorial for an introduction. We also have a full-stack JavaScript guide available in Russian, Portuguese and Spanish.

The Ember Data Value Proposition

An example of how the Ember Data library can help accomodate customer needs.

Let’s begin by considering a simple example.

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:

App.Post = DS.Model.extend({
  tags: DS.hasMany('tag', {async: true})
});

App.Tag = DS.Model.extend({
  post: DS.belongsTo('post', {async: true})
});

to:

App.PostType = DS.Model.extend({
  tags: DS.hasMany('tag', {async: true})
});

App.Post = App.PostType.extend({
});

App.Page = App.PostType.extend({
});

App.Tag = DS.Model.extend({
  posts: DS.hasMany('postType', {polymorphic: true, async: true})
});

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

Before diving into a real world example, let’s review some Ember Data fundamentals.

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):

App.ItemsRoute = Ember.Route.extend({
  model: function(){
    // GET /items
    // Retrieves all items.
    return this.modelFor('orders.show').get('items');
  }
});

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.

Model Attributes

Basic model attributes are defined using DS.attr; e.g.:

App.User = DS.Model.extend({
  firstName: DS.attr('string'),
  lastName:  DS.attr('string')
});

Only fields created by DS.attr will be included in the payload that’s passed to the server for creating or updating records.

DS.attr accepts four data types: string, number, boolean and date.

RESTSerializer

By default:

  • Ember Data employs RESTSerializer for creating objects from API responses (deserialization) and for generating JSON for API requests (serialization).
  • RESTSerializer expects fields created by DS.belongsTo to have a field named user included in the JSON response from the server. That field contains the id of the referenced record.
  • RESTSerializer adds a user field to the payload passed to the API with the id of the associated order.

A response might, for example, look like this:

GET http://api.example.com/orders?ids[]=19&ids[]=28
{
  "orders": [
    {
      "id":        "19",
      "createdAt": "1401492647008",
      "user":      "1"
    },
    {
      "id":        "28",
      "createdAt": "1401492647008",
      "user":      "1"
    }
  ]
}

And an HTTP request created by RESTSerializer for saving an order might look like this:

POST http://api.example.com/orders
{
  "order": {
    "createdAt": "1401492647008",
    "user":      "1"
  }
}

One-to-one Relationships

Say, for example, that each User has a unique Profile. We can represent this relationship in Ember Data using DS.belongsTo on both User and Profile:

App.User = DS.Model.extend({
  profile: DS.belongsTo('profile', {async: true})
});

App.Profile = DS.Model.extend({
  user: DS.belongsTo('user', {async: true})
});

We can then get the association with user.get('profile') or set it with user.set('profile', aProfile).

RESTSerializer expects the ID of the associated model to be provided for each models; e.g.:

GET /users
{
  "users": [
    {
      "id": "14",
      "profile": "1"  /* ID of profile associated with this user */
    }
  ]
}

GET /profiles
{
  "profiles": [
    {
      "id": "1",
      "user": "14"  /* ID of user associated with this profile */
    }
  ]
}

Similarly, it includes the associated model’s ID in a request payload:

POST /profiles
{
  "profile": {
    "user": "17"  /* ID of user associated with this profile */
  }
}

One-to-many and Many-to-one Relationships

Say we have a model where a Post has many Comments. In Ember Data, we can represent this relationship with DS.hasMany('comment', {async: true}) on Post and DS.belongsTo('post', {async: true}) on Comment:

App.Post = DS.Model.extend({
  content: DS.attr('string'),
  comments: DS.hasMany('comment', {async: true})
});

App.Comment = DS.Model.extend({
  message: DS.attr('string'),
  post: DS.belongsTo('post',      {async: true})
});

We can then get associated items with post.get('comments', {async: true}) and add a new association with post.get('comments').then(function(comments){ return comments.pushObject(aComment);}).

The server will then respond with an array of IDs for the corresponding comments on a Post:

GET /posts
{
  "posts": [
    {
      "id":       "12",
      "content":  "",
      "comments": ["56", "58"]
    }
  ]
}

… and with an ID for each Comment:

GET /comments?ids[]=56&ids[]=58
{
  "comments": [
    {
      "id":      "56",
      "message": "",
      "post":    "12"
    },
    {
      "id":      "58",
      "message": "",
      "post":    "12"
    }
  ]
}

RESTSerializer adds the id of the associated Post to the Comment:

POST /comments
{
  "comment": {
    "message": "",
    "post":    "12"    /* ID of post associated with this comment */
  }
}

Note though that, by default, RESTSerializer, will not add DS.hasMany associated IDs to the objects that it serializes, since those associations are specified on the “many” side (i.e., those which have a DS.belongsTo association). So, in our example, although a Post has many comments, those IDs will not be added to the Post object:

POST /posts
{
  "post": {
    "content": ""      /* no associated post IDs added here */
  }
}

To “force” DS.hasMany IDs to be serialized as well, you can use the Embedded Records Mixin.

Many-to-many Relationships

Say that in our model an Author may have multiple Posts and a Post may have multiple Authors.

To represent this relationship in Ember Data, we can use DS.hasMany('author', {async: true}) on Post and DS.hasMany('post', {async: true}) on Author:

App.Author = DS.Model.extend({
  name:  DS.attr('string'),
  posts: DS.hasMany('post', {async: true})
});

App.Post = DS.Model.extend({
  content: DS.attr('string'),
  authors: DS.hasMany('author', {async: true})
});

We can then get associated items with author.get('posts') and add a new association with author.get('posts').then(function(posts){ return posts.pushObject(aPost);}).

The server will then respond with an array of IDs for the corresponding objects; e.g.:

GET /authors
{
  "authors": [
    {
      "id":    "1",
      "name":  "",
      "posts": ["12"]     /* IDs of posts associated with this author */
    }
  ]
}

GET /posts
{
  "posts": [
    {
      "id":      "12",
      "content": "",
      "authors": ["1"]    /* IDs of authors associated with this post */
    }
  ]
}

Since this is a many-to-many relationship, RESTSerializer adds an array of IDs of associated objects; e.g:

POST /posts
{
  "post": {
    "content": "",
    "authors": ["1", "4"]    /* IDs of authors associated with this post */
  }
}

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 with 0 to n Orders) and a One-to-many relationship between Users and Providers (each User may be associated with 0 to n Providers)

      App.User = DS.Model.extend({
        firstName: DS.attr('string'),
        lastName:  DS.attr('string'),
        isAdmin:   DS.attr('boolean'),
    	
        orders:    DS.hasMany('order',    {async: true}),
        providers: DS.hasMany('provider', {async: true})
      });
    
  • One-to-many relationship between Orders and ProviderOrders (each Order consists of 1 to n ProviderOrders):

      App.Order = DS.Model.extend({
        createdAt:      DS.attr('date'),
    	
        user:           DS.belongsTo('user',         {async: true}),
        providerOrders: DS.hasMany('providerOrders', {async: true})
      });
    
  • One-to-many relationship between Providers and ProviderOrders (each Provider may be associated with 0 to n ProviderOrders):

      App.Provider = DS.Model.extend({
        link:   DS.attr('string'),
    	
        admin:  DS.belongsTo('user',          {async: true}),
        orders: DS.belongsTo('providerOrder', {async: true}),
        items:  DS.hasMany('items',           {async: true})
      });
    
  • One-to-many relationship between ProviderOrders and Items (each ProviderOrder consists of 1 to n items):

      App.ProviderOrder = DS.Model.extend({
        // One of ['processed', 'in_delivery', 'delivered']
        status:   DS.attr('string'),
    	
        provider: DS.belongsTo('provider', {async: true}),
        order:    DS.belongsTo('order',    {async: true}),
        items:    DS.hasMany('item',       {async: true})
      });
    	
      App.Item = DS.Model.extend({
        name:     DS.attr('string'),
        price:    DS.attr('number'),
    	
        order:    DS.belongsTo('order',    {async: true}),
        provider: DS.belongsTo('provider', {async: true})
      });
    

And let’s not forget our Route definition:

App.OrdersRoute = Ember.Route.extend({
  model: function(){
    // GET /orders
    // Retrieves all orders.
    return this.store.find('order');
  }
});

Now each ProviderOrder has one Provider, which was our main goal. Items are moved from Order to ProviderOrder and assumption is that all items in one ProviderOrder belong to a single Provider.

Minimizing Code Churn

Unfortunately, there are some breaking changes here. So let’s see how Ember Data can help us minimize any resulting code churn in our code base.

Previously, we were pushing items with items.pushObject(item). Now we need to first find the appropriate ProviderOrder and push an Item to it:

order.get('providerOrders').then(function(providerOrders){
        return providerOrders.findBy('id', item.get('provider.id') )
                             .get('items').then(functions(items){
                               return items.pushObject(item);
                             });
     });

Since this is a lot of churn, and more Order’s job than controller’s, it’s better if we move this code into Order#pushItem:

App.Order = DS.Model.extend({
  createdAt: DS.attr('date'),

  user:      DS.belongsTo('user', {async: true}),
  providerOrders: DS.hasMany('providerOrders', {async: true}),

  /** returns a promise */
  pushItem:  function(item){
    return this.get('providerOrders').then(function(providerOrders){
      return providerOrders.findBy('id', item.get('provider.id') )
                           .get('items').then(functions(items){
                             return items.pushObject(item);
                           });
    });
  }
});

Now we can add items directly on order like order.pushItem(item).

And for listing Items of each Order:

App.Order = DS.Model.extend({
  /* ... */

  /** returns a promise */
  items: function(){
    return this.get('restaurantOrders').then(function(restaurantOrders){
      var arrayOfPromisesContainingItems = restaurantOrders.mapBy('items')

      return arrayOfPromisesContainingItems.then(function(items){
        return items.reduce(function flattenByReduce(memo, index, element){
          return memo.pushObject(element);
        }, Ember.A([]));
      });
    });
  }.property('restaurantOrders.@each.items')
  /* ... */
});

App.ItemsRoute = Ember.Route.extend({
  model: function(){
    // Multiple GET /items with ids[] query parameter.
    // Returns a promise.
    return this.modelFor('orders.show').get('items');
  }
});

Polymorphic Relationships

Let’s now introduce an additional enhancement request to our system that complicates things further:

New requirement #2: Support multiple types of Providers.

For our simple example, let’s say that two types of Providers (“Shop” and “Bookstore”) are defined:

App.Shop = App.Provider.extend({
  status: DS.attr('string')
});

App.BookStore = App.Provider.extend({
  name: DS.attr('string')
});

Here’s where Ember Data’s support for polymorphic relationships will 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 attribute polymorphic: true to the specification of the association. For example:

App.Provider = DS.Model.extend({
  providerOrders: DS.hasMany('providerOrder', {async: true})
});

App.ProviderOrder = DS.Model.extend({
  provider: DS.belongsTo('provider', {polymorphic: true, async: true})
});

The above polymorphic flag indicates that there are various types of Providers that can be associated with a ProviderOrder (in our case, either a Shop or a Bookstore).

When a relationship is polymorphic, the server response should indicate both the ID and the type of the returned object (the RESTSerializer does this by default); e.g.:

GET /providerOrders
{
  "providerOrders": [{
    "status": "in_delivery",
    "provider": 1,
    "providerType": "shop"
  }]
}

Meeting the New Requirements

We need polymorphic Providers and Items to meet the requirements. Since ProviderOrder connects Providers with Items, we can change it’s associations to polymorphic associations:

App.ProviderOrder = DS.Model.extend({
  provider:  DS.belongsTo('provider', {polymorphic: true, async: true}),
  items:     DS.hasMany('item',       {polymorphic: true, async: true})
});

There is a remaining issue, though: Provider has a non-polymorphic association to Items but Item is an abstract type. We therefore have two options to address this:

  1. Require all providers to be associated with the same item type (i.e., declare a specific type of Item for the association with Provider)
  2. Declare the items association on Provider as polymorphic

In our case, we need to go with option #2 and declare the items association on Provider as polymorphic:

App.Provider = DS.Model.extend({
  /* ... */
  items:     DS.hasMany('items', {polymorphic: true, async: true})
});

Note that this does not introduce any code 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:

has_many :users through: :ownerships or Representing Intermediate Models

This will consult a pivot model called Ownership to find associated Users. If the pivot model is basically a pivot table, you can avoid creating an intermediate model in Ember Data and represent the relationship with DS.hasMany on both sides.

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

App.User = DS.Model.extend({
  // Omitted
  ownerships: DS.hasMany('ownership'),

  /** returns a promise */
  providers: function(){
    return this.get('ownerships').then(function(ownerships){
      return ownerships.mapBy('provider');
    });
  }.property('ownerships.@each.provider')
});

App.Ownership = DS.Model.extend({
  // One of ['approved', 'pending']
  status:     DS.attr('string'),
  user:       DS.belongsTo('user', {async: true}),
  provider:   DS.belongsTo('provider', {async: true})
});

App.Provider = DS.Model.extend({
  // Omitted
  ownerships: DS.hasMany('ownership', {async: true}),

  /** returns a promise */
  users: function(){
    return this.get('ownerships').then(function(ownerships){
      return ownerships.mapBy('user');
    });
  }.property('ownerships.@each.user')
});

has_many :mappings, as: locatable

In our ActiveRecord object, we have a typical polymorphic relationship:

class User < ActiveRecord::Base
  has_many :mappings, as: locatable
  has_many :locations, through: :mappings
end

class Mapping < ActiveRecord::Base
  belongs_to :locatable, polymorphic: true
  belongs_to :location
end

class Location < ActiveRecord::Base
  has_many :mappings
  has_many :users,     through: :mappings, source: :locatable, source_type: 'User'
  has_many :providers, through: :mappings, source: :locatable, source_type: 'Provider'

  def locatables
    users + providers
  end
end

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

App.Locatable = DS.Model.extend({
  locations: DS.hasMany('location', {async: true})
});

App.User = App.Locatable.extend({
  userName: DS.attr('string')
});

App.Location = DS.Model.extend({
  locatables: DS.hasMany('locatable', {polymorphic: true, async: true})
});

For Locatables, like User, the server should return IDs for the associated locations:

GET /users
{
  "users": [
    {
      "id": "1",
      "userName": "Pooyan",
      "locations": ["1"]
    }
  ]
}

For Location, the server should return both the ID and type of Locatable in the array of objects:

GET /locations
{
  "locations": [
    {
      "id": "1",
      "locatables": [
        {"id": "1", "type": "user"},
        {"id": "2", "type": "provider"}
      ]
    }
  ]
}

Also, you can represent relationships by type with a static many-to-many relationship:

App.User = App.Locatable.extend({
  userName: DS.attr('string'),
  locations: DS.hasMany('location', {async: true})
});

App.Provider = App.Locatable.extend({
  link: DS.attr('string'),
  locations: DS.hasMany('location, {async: true}
});

App.Location = DS.Model.extend({
  users: DS.hasMany('user', {async: true}),
  providers: DS.hasMnay('provider', {async: true})
});

And what about realtime data?

Ember Data has push, pushPayload and update. You can always manually push new/updated records into Ember Data’s local cache (called store) and it will handle all the rest.

App.ApplicationRoute = Ember.Route.extend({
  activate: function(){
    socket.on('recordUpdated', function(response){
      var type = response.type;
      var payload = response.payload;
      this.store.pushPayload(type, payload);
    });
  }
});

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.

App.ApplicationRoute = Ember.Route.extend({
  activate: function(){
    socket.on('recordUpdated', function(response){
      var type = response.type;
      var id = response.id;

      if( this.store.hasRecordForId(type, id) ){
        this.store.find(type, id);
      }
    });
  }
});

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:

  1. Write an Adapter for your realtime communication channel and use it instead of RESTAdapter.
  2. 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 as route#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:

Modular Design

Ember Data has a modular design under the hood. Key components include:

  1. Adapters are responsible for handling communication, currently only REST over HTTP.
  2. Serializers manage the creation of models from JSON or the reverse.
  3. Store caches created records.
  4. Container glues all these together.

Benefits of this design include:

  1. Deserialization and storage of data works independently of the communication channel employed and resource requested.
  2. Working configurations, such as ActiveModelSerializer or EmbeddedRecordsMixin, are provided out-of-the-box.
  3. Data sources (e.g., LocalStorage, CouchDB implementation, etc.) can be swapped in and out by changing adapters.
  4. Despite much convention over configuration, it is possible to configure everything and share your configuration/implementation with the community.

Sideloading

Ember Data supports “sideloading” of data; i.e., indicating ancillary data that should be retrieved (along with the primary data requested) so as to help consolidate multiple related HTTP requests.

A common use case is sideloading associated models. For example, each Shop has many groceries, so we can include all related groceries in /shops response:

GET /shops
{
  "shops": [
    {
      "id": "14",
      "groceries": ["98", "99", "112"]
    }
  ]
}

When the groceries association is accessed, Ember Data will issue:

GET /groceries?ids[]=98&ids[]=99&ids[]=112
{
  "groceries": [
    { "id": "98",      "provider": "14", "type": "shop" },
    { "id": "99",  "provider": "14", "type": "shop" },
    { "id": "112", "provider": "14", "type": "shop" }
  ]
}

However, if we instead return associated Groceries in /shops endpoint, Ember Data won’t need to issue another request:

GET /shops
{
  "shops": [
    {
      "id": "14",
      "groceries": ["98", "99", "112"]
    }
  ],

  "groceries": [
    { "id": "98",  "provider": "14", "type": "shop" },
    { "id": "99",  "provider": "14", "type": "shop" },
    { "id": "112", "provider": "14", "type": "shop" }
  ]
}

Ember Data accepts links in place of association IDs. When an association specified as a link is accessed, Ember Data will issue a GET request to that link in order to get the associated records.

For example, we could return a link for a groceries association:

GET /shops
{
  "shops": [
    {
      "id": "14",
      "links": {
        "groceries": "/shops/14/groceries"
      }
    }
  ]
}

And Ember Data would then issue a request to /shops/14/groceries

GET /shops/14/groceries
{
  "groceries": [
    { "id": "98",  "provider": "14", "type": "shop" },
    { "id": "99",  "provider": "14", "type": "shop" },
    { "id": "112", "provider": "14", "type": "shop" }
  ]
}

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, ActiveModelSerializer and ActiveModelAdapter are more used in practice than RESTSerializer and RESTAdapter. In particular, when the backend uses Ruby on Rails and the ActiveModel::Serializers gem, the best option is to use ActiveModelSerializer and ActiveModelAdapter, since they support ActiveModel::Serializers out of the box.

Fortunately, though, the usage differences between ActiveModelSerializer/ActiveModelAdapter and RESTSerializer/RESTAdapter are quite limited; namely:

  1. ActiveModelSerializer will use snake_case field names while RESTSerializer requires camelCased field names.
  2. ActiveModelAdapter issues requests to snake_case API methods while RESTSerializer issues to camelCased API methods.
  3. ActiveModelSerializer expects association related field names to end in _id or _ids while RESTSerializer expects association related field names to be the same as the association field.

Regardless of the Adapter and Serializer choice, Ember Data models will be exactly the same. Only JSON representation and API endpoints will be different.

Take our final ProviderOrder as an example:

App.ApplicationSerializer = DS.ActiveModelSerializer.extend({
});

App.ProviderOrder = DS.Model.extend({
  // One of ['processed', 'in_delivery', 'delivered']
  status:    DS.attr('string'),

  provider:  DS.belongsTo('provider', {polymorphic: true, async: true}),
  order:     DS.belongsTo('order', {async: true}),
  items:     DS.hasMany('item', {polymorphic: true, async: true})
});

With Active Model Serializer and Adapter, server should expect:

Post /provider_orders
{
  "provider_order": [
    "status":   "",
    "provider": {"id": "13", "type": "shop"}
    "order_id": "68",
  ]
}

… and should respond with:

GET /provider_orders
{
  "provider_orders": [
    "id":       "1",
    "status":   "",
    "provider": {"id": "13", "type": "shop"}
    "order_id": "68",
    "items": [
      {"id": "57", "type": "grocery"},
      {"id": "89", "type": "grocery"}
    ]
  ]
}

Embedded Records Mixin

DS.EmbeddedRecordsMixin is an extension for DS.ActiveModelSerializer which 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:

  1. Not to serialize or deserialize associations.
  2. To serialize or deserialize associations with id or ids.
  3. To serialize or deserialize associations with embedded models.

This is particularly useful in one-to-many relationships where, by default, DS.hasMany associated 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.

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

App.CartSerializer = DS.ActiveModelSerializer
                       .extend(DS.EmbeddedRecordsMixin)
                       .extend{
                         attrs: {
                           // thanks EmbeddedRecordsMixin!
                           items: {serialize: 'ids', deserialize: 'ids'}
                         }
                       });

App.Cart = DS.Model.extend({
  items: DS.hasMany('item', {async: true})
});

App.Item = DS.Model.extend({
  cart: DS.belongsTo('item', {async: true})
});

As shown in the above example, the EmbeddedRecordsMixin allows for explicit specification of which associations to serialize and/or deserialize via the attrs object. Valid values for serialize and deserialize are: - '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)

The following association modifiers are supported: polymorphic, inverse, and async

Polymorphic Modifier

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

Recall our earlier example of 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, a polymorphic modifier 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 Taggable
App.Page = App.Taggable.extend({});

// A Post is a type of Taggable
App.Post = App.Taggable.extend

App.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 the invers modifier.

Consider, for example, a case where:

  • Each Page may have many Users as Collaborators
  • Each Page may have many Users as Maintainers
  • Each User may have many Favorite Pages
  • Each User may have many Followed Pages

This would need to be specified as follows in our model:

App.User = DS.Model.extend({
  favoritePages:    DS.hasMany('page', {inverse: 'favoritors}),
  followedPages:    DS.hasMany('page', {inverse: 'followers'}),
  collaboratePages: DS.hasMany('page', {inverse: 'collaborators'}),
  maintainedPages:  DS.hasMany('page', {inverse: 'maintainers'})
});

App.Page = DS.Model.extend({
  favoritors:       DS.hasMany('user', {inverse: 'favoritePages'}),
  followers:        DS.hasMany('user', {inverse: 'followedPages'}),
  collaborators:    DS.hasMany('user', {inverse: 'collaboratedPages}),
  maintainers:      DS.hasMany('user', {inverse: 'maintainedPages'})
});

Async Modifier

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.

When async is false, getting associated objects would be done as follows:

post.get('comments').pushObject(comment);

When async is true, getting associated objects would be done as follows (note the callback function specified):

post.get('comments').then(function(comments){
  comments.pushObject(comment);
})

Note well: The current default value of async is false, but in Ember Data 1.0 the default will be true.

‘ids’ Parameter in GET Requests

By default, Ember Data expects that resources aren’t nested. Take Posts which have many Comments as an example. In a typical interpretation of REST, API endpoints might look like these:

GET /users
GET /users/:id/posts
GET /users/:id/posts/:id/comments

However, Ember Data expects API endpoints to be flat, and not nested; e.g.:

GET /users    with ?ids[]=4 as query parameters.
GET /posts    with ?ids[]=18&ids[]=27 as query parameters.
GET /comments with ?ids[]=74&ids[]=78&ids[]=114&ids[]=117 as query parameters.

In the above example, ids is the name of the array, [] indicates that this query parameter is an array, and = indicates that a new ID is being pushed onto the array.

Now Ember Data can avoid downloading resources it already has or defer downloading them to very last moment.

Also, to white list an array parameter using the StrongParameters gem, you can declare it as params.require(:shop).permit(item_ids: []).

Hiring? Meet the Top 10 Freelance Ember.js Developers for Hire in December 2016
Don't miss out.
Get the latest updates first.
No spam. Just great engineering and design posts.
Don't miss out.
Get the latest updates first.
Thank you for subscribing!
You can edit your subscription preferences here.

Comments

Wilhelm Murdoch
Wow, you weren't joking. This is one of the most thorough and well-thought-out JavaScript articles I've read in a long time. For front-end JavaScript, I use AngularJS 100% of time, but I found this article incredibly easy to digest. Good work!
Duke
Amazing article, Thank you!
Ian Petzer
This is a brilliant investigation into Ember Data.. Thanks so much for taking the time to put this together
Ali MX
i must say im impressed !! Amazing Article very helpful and applicable
pekhee
Thanks man! Wilhelm I'm using AngularJS a lot too, however recently my first choice is Ember, believe me, projects do get done faster.
pekhee
Thank _you_ for reading bud. LMK if there is anything missing from article.
pekhee
Thanks Ian. Glad to help anytime.
Pooyan Khosravi
Thanks Ali. ;)
Jhon Wang
The has many through returns a promise, that cannot be used on the handlebars template using each helper. Is there any way to return the array instead of promise?
Joe Daniels
Thankyou for this! Theres so much wonderful stuff here! —gonna read this a couple more times! :)
Millisami
Nice reading. Got full of good stuff paras for Ember-Data.! Thanks a lot!
sub regi
Thanks for this very authoritative article on Ember-Data. Could someone shed light as how to switch to custom identifiers <table_name>+'_id' instead of just 'id'?
km
Unreal article dude. I've been looking around for advice on has_many through associations in Ember for a while and found sketchy pieces of info here and there however you're article nails it. Thanks heaps.
km
Awesome article. Thanks heap for taking the time to write such a comprehensive piece. I've been looking for in depth information about has_many through in Ember for some time.
Pooyan Khosravi
Glad it helped you ;)
Pooyan Khosravi
if you're using ActiveModel::Serializers gem, consider using ActiveModelSerializer and ActiveModelAdapter, they'll serialize records with <model_name>+'_id'. http://www.toptal.com/emberjs/a-thorough-guide-to-ember-data#active-model-serializer-and-adapter
sub regi
Unfortunately not using Rails just a plain RESTAdapter. Would like to minimise change to the back-end REST server.
Vladimir Katansky
Really cool guide! However, I can't use ActiveModelAdapter/Serializer as expected. It looks like it doesn't work with belongsTo relationship :/
Pooyan Khosravi
I'm afraid in this case you need to implement your own serializer. Take a look at `ActiveModelSerializer` source to get an idea.
Pooyan Khosravi
Please try posting a question on stackoverflow. I can assure you `belongsTo` works.
Vladimir Katansky
Yep, I've posted on stackoverflow, but I see that problem it isn't possible to resolve this problem on client using Ember-Data http://stackoverflow.com/questions/24684816/ember-data-save-model-with-belongsto-relationship . The problem is update all related models, consider this: Comment belongs to User and Post and update 2 related models at a time produces "Too many redirections" error :(
sub regi
Really like this article. On the server front, Is there a ember-data compatible basic REST-server multi-model example code that I could refer to in order to fully understand the different scenarios of CRUD and C&D of child objects when there are 1:n and m:n relations. Thanks
Pooyan Khosravi
Not that I know of, however I'm planning to write a blogpost about this and keep a note to write a fully working server so we can have a github project to refer to.
Justin Cypret
Awesome article; the best I have read on complex data types in Ember. I especially enjoyed the parts on polymorphic associations. Very helpful.
guysung kim
First, I would like to say thank you for sharing this. For the following code in above article , App.Provider = DS.Model.extend({ link: DS.attr('string'), admin: DS.belongsTo('user', {async: true}), orders: DS.belongsTo('providerOrder', {async: true}), items: DS.hasMany('items', {async: true}) }); As you pointed out one-to-many relationship between Providers and ProviderOrders, should I correct "orders: DS.belongsTo('providerOrder', {async: true})" to "orders: DS.hasMany('providerOrder', {async: true})"?
Michael Godden
Fantastic article, my knowledge of ember data just tripled. Thanks Pooyan.
John Martinez
Hmmm. I'm totally new to this and just trying to understand as I read, but In the code example for a one-to-one relation, shouldn't the line: user: DS.belongsTo('user', {async: true}) show the reverse relation from user back to profile? user: DS.belongsTo('profile', {async: true})
Juanmnl
Thanks! awesome article! You made me fall in love with ember data :)
Ilya Radchenko
You can also wrap any data that you use from a promise in a {{#if user.isFullfilled}} .. use user here {{/if}} where user is a promise that might not be resolved.
Johnny Oshika
A very thorough post covering a very complicated topic. Thank you!
Matthias Bleyl
Great! BecauseEmber is Client-side framework, an active sending of updated data from server to client seems to be a little bit difficult. For this reason I was happy to find your "What about realtime data" section! Your approach makes sense for me: sending notification about new data via web-sockets, and using the standard REST API for the transfer of the data record(s) (if needed). Anyhow, it does not always work for me, unfortunately: 1 fetching new data records works fine: socket.on("newRecord", function(data){ if(!store.hasRecordForId(data.type, data.id) ) store.find(data.type, data.id); }); 2 however, updating existing data records does NOT work: socket.on("updateRecord", function(data){ if(store.hasRecordForId(data.type, data.id) ) store.find(data.type, data.id); }); --> The store.find() method does not cause a REST API call for existing records, unfortunately. It is still not clear for me how to mark existing records as "invalid", and how to fetch the framework to reload them?
Matthias Bleyl
Just found the solution: store.find(data.type, data.id); ....does not reload an existing data record from server. store.getById(data.type, data.id).reload(); ... seems to work for reloading existing data records.
José Carlos
With ember-data 1.0.0beta12 there is a way to fetch a record from the server even when its already on the local store. Just use store.fetch(data.type, data.id) ... For more info see the link below: http://emberjs.com/blog/2014/11/24/ember-data-1-0-beta-12-released.html
子不语
I got a question: how should I get records from server with url like 'post/:id/comments' in a Ember Data's way. Thanks in advance~
Test_Customer
Wow, This is a fantastic investigation. forward to see more like this .good work !!
Thomas van Lankveld
Cool article! Would you by any chance know how I could build something that would allow me to do: {{#if user.hasPosts}} in my templates, without having these posts loaded yet?
Mark Duan
I finally knew how to use Ember-data relationship, thanks a lot.
Frank Treacy
Brilliant, thanks Pooyan! This authoritative post is so useful. I found myself several times coming back here for reference instead of the docs :) For those of you interested in a more updated guide (Ember Data 1.13, JSON API) I'd like to share this post: http://emberigniter.com/modern-bridge-ember-and-rails-5-with-json-api/ (I'm the author)
Aaron
Yo, date your blog posts. It can be so hard to tell if Ember information is up to date
auvipy
Hi, great writing!! could you please manage some time to make this tuto up-to-date with atleast ember-2.8?
comments powered by Disqus