Migrating from Ember Data 0.13 to 1.0.0 Beta 1 - My findings

I decided to convert a small inhouse project from Ember Data 0.13 to 1.0.0 Beta. Below is an overview of my findings (the changes needed, some sample code and some walls I hit). I hope this info is useful for others. Do not hesitate to post any additional info as a comment to this post.

I will focus in this post on the CRUD actions of simple entities (not containing related or embedded data). In a later post I will come back on some specifics related to embedded records.

Please read the Ember data transition guide before starting any migration: https://github.com/emberjs/data/blob/master/TRANSITION.md

1. Use the latest master of Ember Data 1.0.0

During the migration I have encountered certain bugs and these have in the meantime been solved on the master version. So I recommend to use http://builds.emberjs.com/ember-data-latest.js for now (is in any case still beta) !

2. Pluralization out of the box

In 0.13, specific pluralization (e.g. category > categories) needed to be set via DS.RESTAdapter.configure(ā€œpluralsā€, { ā€¦ }). This is no longer needed. There is now a default ā€œsmartā€ pluralization taking care of this.

See: https://github.com/emberjs/data/commit/9325a1dea594b8ff752886eb7a9d752785282e07

3. RESTAdapter endpoint customization

In 0.13, a specific endpoint (host) or namespace could be set via DS.RESTAdapter.reopen({ ā€¦}). This is still possible (in the master version but not in the beta 1.0.0 version), but please note that ā€œurlā€ is changed to ā€œhostā€. Correct code is:

DS.RESTAdapter.reopen({  
    host: "http://api.company.com";
});

4. Per type serializers and per type adapters

As of version 1.0.0, serializers and adapters are per model type. If you have specific serializer needs (e.g. your idā€™s are returned as _id), then you should specify for each model type a specific serializer. I have created a custom serializer and then specified for each model type this serializer. See below:

App.Serializer = DS.RESTSerializer.extend({
  //Custom serializer used for all models
  normalize: function (type, property, hash) {
    // normalize the `_id`
    var json = { id: hash._id };
    delete hash._id;

    // normalize the underscored properties
    for (var prop in hash) {
      json[prop.camelize()] = hash[prop];
    }

    // delegate to any type-specific normalizations
    return this._super(type, property, json);
  }
});

//Extend each model with the custom serializer
App.AuthorSerializer = App.Serializer.extend();
App.CategorySerializer = App.Serializer.extend();

5. Reading/finding data

Straightforward code change required for fetching data.

Retrieving all records:

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

Retreiving a single record:

 App.AuthorsEditRoute = Ember.Route.extend({
   model: function (params) {
     return this.store.find('author', params.author_id);
   }
 });

6. Creating a new record and saving it

Creation of record:

App.AuthorsNewRoute = Ember.Route.extend({
  model: function() {
    return this.store.createRecord('author');
  }
 });

Save record (in controller):

Please note that there are no more transactions. You can (need) to refactor all your code that is used to rollback transactions, to recover from becameError or becameInvalid state, etc.

All data actions now have promises and you can thus easily use these to determine whether or not a record is correctly saved (or for example to retry saving records in case of failure). In the code below, I save a record and I catch validation errors (422 response) and other errors (e.g. REST adaper not running).

App.AuthorsNewController = Ember.ObjectController.extend({
  actions: {
    save: function () {
      var self = this;
      var author = self.get('model');
          
      author.save().then(
        function () {
          //Succesful save, thus transition to edit route
          self.transitionToRoute('authors.edit', author);
        },
        function (error) {    
          if (error.status == 422) {
            //422: validation error
            //Put json responsetext into validationErrors obj
            self.set('validationErrors', jQuery.parseJSON(error.responseText));
          } else {
            console.log("Validation error occured - " + error.responseText);
            alert("An error occured - REST API not available - Please try again");
          }
        }
      );
    }  

In the code above, I convert - in case of a 422 error - the responseText into an object and use it to display the validation errors on screen (via handlebars in the hbs).

7. Reverting changes

In previous versions you could use transaction.rollback(). There is now a similar way; you can use record.rollback(). Make sure to use the master version of Ember Data as this did not work in 1.0.0 beta 1. Here is how to rollback an edit of a model (e.g. press the cancel button after having modified a field in a form).

cancel: function () {
      var author = this.get('model');
      author.rollback();
    },

8. Deleting a record

Things become more complicated now. To delete a record, you first need to delete it from the store and then ā€œsaveā€. The save invokes the call to the REST API. And in case of an error, you can always rollback so that the record becomes available again (and thus client model in sync with server). See code below:

deleteAuthor: function () {
      if (confirm("Are you sure you want to delete the selected author ? Click OK to continue.")) {
        var self = this;
        var author = self.get('model');

        //deletes record from store
        author.deleteRecord();
        
        //persist change 
        author.save().then(
          function () {
            //delete succesful, go back to overview
            self.transitionToRoute('authors.index');
          },
          function (error) {
            //Not succsefull, rollback the delete action
            author.rollback();
            alert("An error occured - Please try again");
          }
        );
      }
    }

9. Use Ember Inspector (data view)

As of version 1.0.0, you will no longer receive inFlight errors or errors due to the state being dirty, invalid, etc. However, this does not mean that you cannot mess up things. If you create a record and you browse to another route without saving the record, then you end-up with differences in records at the client (host records with no id) and the server. The same applies to deleting records.

I therefore recommend to keep at all times the Ember Inspector open so that you can verify that the models are as you expect them to be.

Below is a code snipped on how to recover from a route transition in case the data is dirty (changed and not yet saved).

willTransition: function (transition) {
      var model = this.get('currentModel');

      if (model && model.get('isDirty')) {
        if (confirm("You have unsaved changes. Click OK to stay on the current page. Click cancel to discard these changes and move to the requested page.")) {
          //Stay on same page and continue editing
          transition.abort();
        } else {
          //Rollback modifications
          var author = this.get('currentModel');
          author.rollback();
          // Bubble the `willTransition` event so that parent routes can decide whether or not to abort.
          return true;
        }
      } else {
        // Bubble the `willTransition` event so that parent routes can decide whether or not to abort.
        return true;
      }
    }

10. The good news

All in all I had to do a lot of changes. And so far I am only working with very simple data (having no relationships or embedded records).

Thanks to the new version, I could reduce the code in a specific controller (as an example I took my authorsController) from 220 lines to 90 lines. There is no longer a need to manually keep statusses such as ā€œstartEditingā€, ā€œstopEditingā€ and then depending on these rolling back or committing transactions. In a specific route (as an example I took my authorsRoute), I could reduce from 130 to 70 lines.

The code is thus (in my specific case) reduced to approx. 50% and that is good !

The good news is also that during the migration I got prompt feedback and responses to the questions I had (via stackoverflow and twitter). Many thanks to the people who helped me !

Marc

12 Likes

Could you share some more details regarding ā€œ4. Per type serializers and per type adaptersā€? Are you using Rails to serve JSON? Does your JSON return from the server with under_scored attribute names?

After adding the custom serializer you show (from the transistion guide), reading data from my Rails app worked. However, the JSON sent when POSTing data to my server had camelCase attributes. Following the comments here https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L459 I overrode serialize in my custom serializer to return under_scored attribute names. This worked, though I was surprised you didnā€™t mention it. Am I missing something?

1 Like

Do you mind posting an example of how you over wrote the serialize in your custom adapter to work with rails.

Sure. I basically copied https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L462-L464

Here is my full custom serializer:

App.Serializer = DS.RESTSerializer.extend({
  normalize: function(type, property, hash) {
    var json = {};

    for (var prop in hash) {
      json[prop.camelize()] = hash[prop];
    }

    return this._super(type, property, json);
  },

  serialize: function(record, options) {
    var json = {};

    record.eachAttribute(function(name) {
      json[name.underscore()] = record.get(name);
    });

    return json;
  }
});

Works great for simple resources. I have not tried it with relationships yet. Notice that https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L466-L470 has some code for relationships. I have no idea if I need this yet or not.

1 Like

Thanks for adding the code from your custom serializer. cheers.

Thanks for great post. But what we have to do if some other model is embedded like this? The map function ā€˜mapā€™ of adapter would not found.

var adapter = DS.RESTAdapter.reopen({ namespace: ā€˜apiā€™ });

adapter.map( App.Comment, { reporter: { embedded: ā€˜alwaysā€™ } });

App.Store = DS.Store.extend({ revision: 13, adapter: adapter });

For now to set Store adapter you should do this:

App.ApplicationAdapter = DS.FixtureAdapter
App.SomeModelAdapter = AnotherAdapter

Donā€™t create objects like Date in fixtures yourself! Now all this stuff goes through Transform.

v0.13
App.MyModel.FIXTURES = [{id: 1, date: new Date(1999, 12, 31)}];

v1.0
App.MyModel.FIXTURES = [{id: 1, date: '1999-12-31'}];

What we should do if one of the json property from server is null? For example I would to map ā€œreporterā€: null

but I get an error: Cannot call method ā€˜toStringā€™ of undefined

For creating a new record, If I want to store server error messages in modelā€™s errors property, is there a way to do it in model level instead of someModel.save().then(null, failCallback) ?

Thereā€™are 2 callbacks becameInvalid and becameError in model level. But it seems theyā€™re deprecated. Actually the code is still in the repository, but the associated tests are commented out.

I think the only way is to use the .then() way; thus with promises. The callbacks becameInvalid and becameError are indeed deprecated.

Have you run into an issue loading hasMany data on your default route? Since upgrading none of the records in my hasMany relationships are loading in my views. I checked the data and they are being saved properly so Iā€™m guessing this is an async issue.

My model looks like this:

App.List = DS.Model.extend({
	listName : DS.attr( ),
	cards : DS.hasMany( 'card' )
});

And my view code is:

{{#each controller itemController="list"}}  //loop through all lists
    <section class="list list-inline">
        <h1>{{listName}}</h1>
        {{#each card in cards itemController="card"}}
            ...
       {{/each}}
    </section>
{{/each}}

I have a similar problem when I try to save the relationships. At that moment, they all disappear. I use Ember Data 1.0.0 Beta 2.

Might be related to issues:

https://github.com/emberjs/data/issues/1228
https://github.com/emberjs/data/pull/1257

I will watch resolution of these 2 issues and then test again. At this moment, hasMany relationship does not work in my case ā€¦

1 Like

Turns out my issue was with the fixture adapter. I updated to the latest code in the master branch on GitHub and it works fine.

Does someone have a synopsis or notes on all default behaviors of the serializer in the latest version of Ember Data?

Am I correct to assume that the default implementation of the normalize method DOES NOT actually camelize the underscored keys? It looks like that in the source code but maybe I am missing something. Have not spent too much time in the Ember Data source yet.

In other words you must override and implement a normalize method that calls the camelize method on the keys yourself.

Is this true?

What vanilla behaviors can we expect from Ember Data?

1 Like

OK, if I interpret the migration guide correctly, the camelization logic should happen on the server side.

In 0.13, the REST Adapter automatically camelized incoming keys for you. It also expected belongsTo relationships to be listed under name_id and hasMany relationships to be listed under name_ids.

In the future, this logic will live in an ActiveModelSerializer that is designed to work with Rails, and which will ship with ember-rails.

So is it now considered a best practice to use camelized key?

Which is considered better form in general?

{
  "records": [
  {
    "id": "1",
    "some_property": "Some property value"
  },
  {
    "id": "2",
    "some_property": "Another property value"
  }
 ]
}

Or

{
  "records": [
  {
    "id": "1",
    "someProperty": "Some property value"
  },
  {
    "id": "2",
    "someProperty": "Another property value"
  }
 ]
}

Sounds like the latter, is the view of the core team now.

@wycats, @tomdale care to comment?

1 Like

The migration guide explains what Ember Data is expecting. You donā€™t need to camelize on the server, but if you get underscored data back, you will need to normalize the keys in normalize.

A description on how to do this is in the transition guide. @bradleypriest is also working on an adapter that will have the behavior (and other previous, Rails-derived behavior) built-in, and it will be released soon. In the meantime you can check out the PR. Youā€™ll note that itā€™s quite simple and you should be able to use it to adapt to your own conventions.

Ahh, thanks for the clarification. When I first read the migration guide I assumed it was referring to the active model serializer gem that was part of the rails api project. Not a custom serializer in Ember Data.

So basically this is a way of make a specific serializer that conforms to the out of the box experience of the output provided by a rails project that uses Active Model Serializer for JSON creation.

When this get documented might want to be careful about naming. It might be really easy to get confused or hard to search for.

I think when discussing this in the Ember context we should always say

DS.ActiveModelSerializer

@jagthedrummer comment about ā€œUnderscoredRESTSerializerā€ seems apropos.

Iā€™m working through a migration from r13 to beta 2 of Ember data and Iā€™m having some of the same issues. Previously when I called save on a Message model Ember would call /messages with a POST. After the migration Ember is calling /accounts/<id>/messages. Is there a way to change this behaviour back?

My models:

// Model definitions
Social.Account = DS.Model.extend({
    username: DS.attr("string"),
    messages: DS.hasMany("message")
});

Social.Message = DS.Model.extend({
    text: DS.attr("string"),
    account: DS.belongsTo("account")
});
1 Like

try saving only the record and not the relationship instead of both, or check that your json response is returning all data including the relations