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.
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.
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.
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.
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.
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.
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?
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.
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.
Diagrams: Unloaded vs. Loaded
- One-to-one
- One-to-many
- Many-to-many
- Explicit Inverses
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')
});
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')
});
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')
});
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'
})
});
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);
});
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
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.
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
});
}
});
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'
The Ember Data store provides a simple interface for finding records of a single type through the store
object's find
method.
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
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.
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
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.
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'
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);
}
})
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.
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
.
Actions map to the following URLs in the REST adapter:
Action | HTTP Verb | URL |
---|---|---|
Find | GET | /people/123 |
Find All | GET | /people |
Update | PUT | /people/123 |
Create | POST | /people |
Delete | DELETE | /people/123 |
When requesting a record, the REST adapter expects your server to return a JSON representation of the record that conforms to Ember Data conventions.
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 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
}
}
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';
}
});
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"
}]
}
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')
});