introduction-to-ember-data



introduction-to-ember-data

0 0


introduction-to-ember-data

Slides for Ember.js Indianapolis Meetup

On Github jheth / introduction-to-ember-data

Ember Data

Joe Heth

@jheth

http://github.com/jheth

Features

  • Data persistence library
  • Models & Relationships
  • Adapters (Protocol Abstraction)
  • Serializers
  • Async/Lazy Loading
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. A serializer is responsible for turning a raw JSON payload returned from your server into a record object.

The Store

  • Client Cache (in-browser)
  • Model Lifecycle (new, dirty, deleted)
  • Available to all controllers and routes
  • Central repository of records (Single Source of Truth)
  • Operations return a Promise
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.

Adapters

Know how to talk to the server

  • REST (Default)
  • ActiveModel (Rails + AMS gem)
  • Fixture
  • LocalStorage (kurko/ember-localstorage-adapter)
window.Blog = Ember.Application.create();

Blog.ApplicationAdapter = DS.LSAdapter.extend({
  namespace: 'blog-emberjs'
});
Can have model specific adapters

URL Conventions

Rest Adapter

Action HTTP Verb URL Find GET /posts/123 Find All GET /posts Create POST /posts Update PUT /posts/123 Delete DELETE /posts/123

Model

var Post = DS.Model.extend({
  title: DS.attr('string'),
  body: DS.attr('string'),
  user: DS.belongsTo('user'),
  comments: DS.hasMany('comment'),
  createdAt: DS.attr('date'),
  isPublished: DS.attr('boolean', {
    default: false
  })
});

Model Lifecycle

  • isNew
  • isValid
  • isLoaded
  • isDirty
  • isSaving (in-flight)
  • isDeleted
  • isError
isLoaded - The adapter has finished retrieving the current state of the record from its backend. isDirty - The record has local changes that have not yet been saved by the adapter. This includes records that have been created (but not yet saved) or deleted. isSaving - The record has been sent to the adapter to have its changes saved to the backend, but the adapter has not yet confirmed that the changes were successful. isDeleted - The record was marked for deletion. When isDeleted is true and isDirty is true, the record is deleted locally but the deletion was not yet persisted. When isSaving is true, the change is in-flight. When both isDirty and isSaving are false, the change has been saved. isError - The adapter reported that it was unable to save local changes to the backend. This may also result in the record having its isValid property become false if the adapter reported that server-side validations failed. isNew - The record was created locally and the adapter did not yet report that it was successfully saved. isValid - No client-side validations have failed and the adapter did not report any server-side validation failures.

Relationships

Side Loading

{
  "post": {"title": 'My Post', "comment_ids": [1,2,3], "user_id": 1},
  "comments": [
    {'id': 1, ...}, {'id': 2, ...}
  ],
  "users": [
    {'id': 1, ...}
  ]
}

Embedded Mixin

App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    comments: { embedded: 'always' }
  }
});
Explicit inverses

Async

  comments: DS.hasMany('comment', {async: true})

Embedded IDs, but no data.

{
  "post": {
    "title": "Ember Data",
    "body": "How it works...",
    "comment_ids": [1,2,3]
  }
}

Lazy Load Comments

post.get('comments') => GET "/comments?ids[]=1,ids[]=2,ids[]=3"

Async with Links

  comments: DS.hasMany('comment', {async: true})
  user: DS.belongsTo('user', {async: true})
// Links
{
  "post": {
    "title": "Ember Data",
    "body": "How it works...",
    "links": {
      "comments": "/post/1/comments",
      "user": "/user/9"
    }
  }

Lazy Load Comments

post.get('comments') => GET "/post/1/comments"
post.get('user') => GET "/user/9"

Find

// Array
this.store.find('post')
GET "/posts" => [ {post: {id: 1, ...} }, {post: {id: 2, ...} } ]

// Find Multiple
this.store.find('post', {ids: [1,2,3]});
GET "/posts?ids[]=1&ids[]=2&ids[]=3" => [{},{},{}]

// Query String parameters
this.store.find('post', {hasComments: true, sort_by: 'created_at'})
GET "/posts?hasComments=true&sort_by=created_at" => [{post: ...}]
// Single Record
this.store.find('post', 1)
GET "/posts/1" => {post: {id: 1, ...} }
// Return local records
var posts = this.store.all('post'); // => no network request

Create

var post = this.store.createRecord('post', {
  title: 'Ember Data',
  body: 'Lorem ipsum'
});

// Set relationship after object is loaded.
this.store.find('user', 1).then(function(user) {
  post.set('author', user);
});
The store object is available in controllers and routes using this.store.

Pushing

this.store.push('post', {
  id: 1,
  title: "Ember.js",
  body: "A javascript framework."
});

// Patch
var updateEvent = {id: 1, title: "New Title"};
this.store.update('post', updateEvent);
var pushData = {
  posts: [
    {id: 1, post_title: "Great post", comment_ids: [2]}
  ],
  comments: [
    {id: 2, comment_body: "Insightful comment"}
  ]
}

this.store.pushPayload(pushData);
All objects in Ember must have an 'id' attribute.

Update

var user = this.store.find('user', 9);

// ...after the record has loaded
user.incrementProperty('postCount');
user.set('isAdmin', true);

person.get('isDirty');    // => true
user.changedAttributes(); // => { isAdmin: [false, true], postCount: [2, 3] }

user.save();              // => POST /users/9
// OR
user.rollback();          // => reverts local state
user.get('isDirty');      // => false

// OR
user.reload();            // => GET /users/9
Promise is always returned

Delete

this.store.find('post', 1).then(function (post) {
  post.deleteRecord();
  post.get('isDeleted'); // => true
  post.save(); // => DELETE /posts/1
});

// OR
this.store.find('post', 2).then(function (post) {
  post.destroyRecord(); // => DELETE /posts/2
});

Unloading

Removes from local store, no network request

// Remove single record
this.store.find('post', 1).then(function(post) {
  post.unloadRecord();
  // OR
  this.store.unloadRecord(post);
});

// Remove all from local store.
this.store.unloadAll( 'post' );

Metadata

Ember Data's JSON deserializer looks for a meta key:

var result = this.store.find("post", { limit: 10, offset: 0 });
{
  "posts": [
    { "id": 1, "title": "My First Blog Post" },
    { "id": 2, "title": "My Second Blog Post" }
  ],
  "meta": {
    "total": 100,
    "page": 0
  }
}
var meta = this.store.metadataFor("post");
// meta.total => 100

// Or you can access the metadata just for this query:

var meta = result.get("content.meta");

Resources

https://speakerdeck.com/dgeb/optimizing-an-api-for-ember-data