Skip to content

Instantly share code, notes, and snippets.

@pixelhandler
Last active November 27, 2017 14:48
Show Gist options
  • Save pixelhandler/b36cbb91efcf15f9dae7 to your computer and use it in GitHub Desktop.
Save pixelhandler/b36cbb91efcf15f9dae7 to your computer and use it in GitHub Desktop.
Introduction to Ember Data, notes from a talk at Ember-SC in Jan. '14

Introduction to Ember Data

What is Ember Data?

Ember Data is a library that integrates tightly with Ember.js to make it easy to retrieve records from a server, cache them for performance, save updates back to the server, and create new records on the client.

Ember Data can load and save records and their relationships served via a RESTful JSON API, provided it follows certain conventions.

Ember.js

It's important to remember what makes the web special, "the web derives its power from the ability to bookmark and share URLs".

Ember.js marries the tools and concepts of native GUI frameworks with support for the feature that makes the web so powerful: the URL

If you're not building an application driven by data, there are tools for that. Ember.js may not be the strongest fit for every web application. If your application is primarily reading and consuming data, again Ember.js may not be the strongest fit for that need.

However, if you're building a web application that supports users engaging and interacting with data, speaking JSON to your application server, then Ember.js is an excellent fit.

Should you chose Ember Data?

There are alternatives:

  • DIY Store/Adapter (Sockets?)
  • Ember Model
  • Ember Persistence Foundation

When using REST-like web service, Ember Data is a great fit. The RESTAdapter/Serializer and ActiveModelApapter/Serializer do the heavy lifting out of the box, and the Store finds, caches, loads related records pretty well.

Are there bugs? Yes, this is beta software, get to know them by following the pull requests and tune into the converstations in the forums.

Core Concepts

Store

The store is the central repository of records in your application. You can think of the store as a cache of all of the records available in your app. Both your application's controllers and routes have access to this shared store; when they need to display or modify a record, they will first ask the store for it.

This instance of DS.Store is created for you automatically and is shared among all of the objects in your application.

Models

A model is a class that defines the properties and behavior of the data that you present to the user. Anything that the user expects to see if they leave your app and come back later (or if they refresh the page) should be represented by a model.

Records

A record is an instance of a model that contains data loaded from a server. Your application can also create new records and save them back to the server.

Records are uniquely identified by two things:

  • A model type.
  • A globally unique ID.

Adapter

An adapter is an object that knows about your particular server backend and is responsible for translating requests for and changes to records into the appropriate calls to your server.

For example, if your application asks for a person record with an ID of 1, how should Ember Data load it? Is it over HTTP or a WebSocket? If it's HTTP, is the URL /person/1 or /resources/people/1?

Serializer

A serializer is responsible for turning a raw JSON payload returned from your server into a record object.

When the adapter gets a payload back for a particular record, it will give that payload to the serializer to normalize into the form that Ember Data is expecting.

Automatic Caching

The store will automatically cache records for you. If a record had already been loaded, asking for it a second time will always return the same object instance. This minimizes the number of round-trips to the server, and allows your application to render UI to the user as fast as possible.

Architecture Overview

Diagrams: Unloaded vs. Loaded

Defining Relationships

  • One-to-one
  • One-to-many
  • Many-to-many
  • Explicit Inverses

One-to-one

To declare a one-to-one relationship between two models, use DS.belongsTo:

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

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

One-to-many

To declare a one-to-many relationship between two models, use DS.belongsTo in combination with DS.hasMany.

App.Post = DS.Model.extend({
  comments: DS.hasMany('comment')
});

App.Comment = DS.Model.extend({
  post: DS.belongsTo('post')
});

Many-to-many

To declare a many-to-many relationship between two models, use DS.hasMany.

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

App.Tag = DS.Model.extend({
  posts: DS.hasMany('post')
});

Explicit Inverses

Sometimes you may have multiple belongsTo/hasManys for the same type. You can specify which property on the related model is the inverse using DS.hasMany's inverse option.

var belongsTo = DS.belongsTo, hasMany = DS.hasMany;

App.Comment = DS.Model.extend({
  onePost: belongsTo('post'),
  twoPost: belongsTo('post'),
  redPost: belongsTo('post'),
  bluePost: belongsTo('post')
});

App.Post = DS.Model.extend({
  comments: hasMany('comment', {
    inverse: 'redPost'
  })
});

Creating and Deleting Records

Creating Records

You can create records by calling the createRecord method of this.store.

Watch out... that you cannot assign a promise as a relationship.You can easily set the relationship after the promise has fulfilled

var store = this.store;

var post = store.createRecord('post', {
  title: 'Rails is Omakase',
  body: 'Lorem ipsum'
});

store.find('user', 1).then(function(user) {
  post.set('author', user);
});

Deleting Records

Call deleteRecord() on an instance of DS.Model; which flags the record as isDeleted and removes it from all() queries on the store.

The deletion can then be persisted using save().

Alternatively, you can use the destroyRecord method to delete and persist at the same time.

var post = store.find('post', 1);

post.deleteRecord();

post.get('isDeleted'); // => true

post.save(); // => DELETE to /posts/1

// OR

var post = store.find('post', 2);

post.destroyRecord(); // => DELETE to /posts/2

Pushing Records

One way to think about the store is as a cache of all of the records that have been loaded by your application.

Instead of waiting for the app to request a record, however, you can push records into the store's cache ahead of time.

Another use case for pushing in records is if your application has a streaming connection to a backend.

Application Route

To push a record into the store, call the store's push() method.

Preload some data using the ApplicationRoute is the top-most route in the route hierarchy, its model hook gets called once when the app starts up.

App.Album = DS.Model.extend({
  title: DS.attr(),
  artist: DS.attr(),
  songCount: DS.attr()
});

App.ApplicationRoute = Ember.Route.extend({
  model: function() {
    this.store.push('album', {
      id: 1,
      title: "Fewer Moving Parts",
      artist: "David Bazan",
      songCount: 10
    });
  }
});

Persisting Records

Records in Ember Data are persisted on a per-instance basis. Call save() on any instance of DS.Model and it will make a network request.

var post = store.createRecord('post', {
  title: 'Rails is Omakase',
  body: 'Lorem ipsum'
});

post.save(); // => POST to '/posts'

Finding Records

The Ember Data store provides a simple interface for finding records of a single type through the store object's find method.

Finding All Records of a Type

var posts = this.store.find('post'); // => GET /posts

To get a list of records already loaded into the store, without making another network request, use all instead.

var posts = this.store.all('post'); // => no network request

Caveat: Returns Promise Array

find returns a DS.PromiseArray that fulfills to a DS.RecordArray and all directly returns a DS.RecordArray.

It's important to note that DS.RecordArray is not a JavaScript array.

Finding a Single Record

Provide a number or string as the second argument to store.find(). This will return a promise that fulfills with the requested record:

var aSinglePost = this.store.find('post', 1); // => GET /posts/1

Querying For Records

If you provide a plain object as the second argument to find, Ember Data will make a GET request with the object serialized as query params.

This method returns DS.PromiseArray in the same way as find with no second argument.

Query Example

We could search for all person models who have the name of Peter:

var peters = this.store.find('person', { name: "Peter" }); // => GET to /persons?name='Peter'

Integrating with the Route's Model Hook

Routes are responsible for telling their template which model to render.

Ember.Route's model hook supports asynchronous values out-of-the-box. If you return a promise from the model hook, the router will wait until the promise has fulfilled to render the template.

App.Router.map(function() {
  this.resource('posts');
  this.resource('post', { path: ':post_id' });
});

App.PostsRoute = Ember.Route.extend({
  model: function() {
    return this.store.find('post');
  }
});

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('post', params.post_id);
  }
})

REST Adapter

By default, your store will use DS.RESTAdapter to load and save records. The RESTAdapter assumes that the URLs and JSON associated with each model are conventional.

If you follow the conventions, you will not need to configure the adapter to get started.

URL Conventions

The REST adapter determines the URLs it communicates with, based on the name of the model.

Ask for a Post by ID:

var post = store.find('post', 1);

The REST adapter will automatically send a GET request to /posts/1.

Verbs and Nouns

Actions map to the following URLs in the REST adapter:

ActionHTTP VerbURL
FindGET/people/123
Find AllGET/people
UpdatePUT/people/123
CreatePOST/people
DeleteDELETE/people/123

JSON Conventions

When requesting a record, the REST adapter expects your server to return a JSON representation of the record that conforms to Ember Data conventions.

JSON Root

The primary record being returned should be in a named root.

If you request a record from /people/123, the response should be nested inside a property called person.

{
  "person": {
    "firstName": "Jeff",
    "lastName": "Atwood"
  }
}

Attribute Names

Attribute names should be camelized. For example, if you have a model like this:

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

  isPersonOfTheYear: DS.attr('boolean')
});

The JSON returned from your server should look like this:

{
  "person": {
    "firstName": "Barack",
    "lastName": "Obama",
    "isPersonOfTheYear": true
  }
}

Relationships

References to other records should be done by ID. For example, if you have a model with a hasMany relationship:

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

The JSON should encode the relationship as an array of IDs:

{
  "post": {
    "comments": [1, 2]
  }
}

Comments for a post can be loaded by post.get('comments'). The REST adapter will send a GET request to /comments?ids[]=1&ids[]=2&ids[]=3.

Any belongsTo relationships in the JSON representation should be the camelized version of the Ember Data model's name, with the string Id appended. For example, if you have a model:

App.Comment = DS.Model.extend({
  post: DS.belongsTo('App.Post')
});

The JSON should encode the relationship as an ID to another record:

{
  "comment": {
    "post": 1
  }
}

If needed these naming conventions can be overwritten by implementing the keyForRelationship method.

 App.ApplicationSerializer = DS.RESTSerializer.extend({
   keyForRelationship: function(key, relationship) {
      return key + 'Ids';
   }
 });

Sideloaded Relationships

To reduce the number of HTTP requests necessary, you can sideload additional records in your JSON response. Sideloaded records live outside the JSON root, and are represented as an array of hashes:

{
  "post": {
    "id": 1,
    "title": "Node is not omakase",
    "comments": [1, 2]
  },

  "comments": [{
    "id": 1,
    "body": "But is it _lightweight_ omakase?"
  },
  {
    "id": 2,
    "body": "I for one welcome our new omakase overlords"
  }]
}

Creating Custom Transformations

In some circumstances, the built in attribute types of string, number, boolean, and date may be inadequate. For example, a server may return a non-standard date format.

Ember Data can have new JSON transforms registered for use as attributes.

App.CoordinatePointTransform = DS.Transform.extend({
  serialize: function(value) {
    return [value.get('x'), value.get('y')];
  },
  deserialize: function(value) {
    return Ember.create({ x: value[0], y: value[1] });
  }
});
App.Cursor = DS.Model.extend({
  position: DS.attr('coordinatePoint')
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment