I’ve been a fan of Ember Data since the day I started working with Ember.js. It felt so easy in the beginning, I could just create a model and it would work out of the box with my JSON API. Life was fun.
I started using it more and more and unfortunately started hitting a brick wall every time I had a problem. Ember Data is about 8600 lines of code (not including tests) and a huge beast to debug, especially when you have no idea what is going on. Not to mention all the layers of abstraction you need to keep in your head when you actually try to debug something.
But then Erik Bryn came to rescue with his let’s fix the AJAX thing right now library called Ember Model. It was so small that you could read its source code in a couple of minutes and understand how it works. Many people despised it’s simplicity and never gave it a try.
The guys behind Ember Data recognized there were issues with the DS.RESTAdapter
adapter and came up with an answer - DS.BasicAdapter. It felt like a salvation for a little while, allowing people to stay with Ember Data instead of moving somewhere else, but then it got removed as it was not the way they wanted to take.
If you were to ask someone on the core team about AJAX libraries for Ember, they would probably tell you something like this:
Use Ember Data if you have your own API which you can control (and preferrably use activemodelserializers), and use Ember Model only if your API is non-standard. You should be also ready to implement a lot of stuff by hand, since it doesn’t support many features of Ember Data.
Having less baked-in features is actually a feature of Ember Model. It allows for much easier understanding of its codebase and even extension. It doesn’t take much time to understand the flow of execution throughout the whole codebase.
It all starts with creating your own Adapter. If you’re coming from Ember Data you might be thinking that this will be the pain part, but actually Ember Model Adapter is a completely different beast. Ember.RESTAdapter is a little under 150 lines of code and it is very easy to follow. You could easily write it from scratch as we’re going to do later in this article.
Let’s take a look at an example of what happens when you fetch a single record via App.Model.find(1)
.
findById
, since you’re searching for a single ID. If you were to do App.Model.find([1,2])
this would result in a findMany
call (which we’ll later discover is quite similar). 2. cachedRecordForId
first checks the identity map (which is simply available at App.Model.recordCache
to see if there’s already a record for the given ID. It will either return it, or give us a blank new record which will be later populated with data.ID
is added to the current batch of IDs, which is scheduled to execute. 4. We get back our record from step 2.You might be asking what is this magical batch of IDs. It might look like magic at first when you see that these two code snippets are equal
App.Model.find(1);
App.Model.find(2);
App.Model.find([1, 2]);
Even though this looks magical at first there is a simple answer, which lies in the following line of code
Ember.run.scheduleOnce('data', this, this._executeBatch);
Each findById
and findMany
will push the requested IDs into the batch and schedule the execution at the end of the current runloop. Meaning multiple calls to the same model will be coalesced into a single findMany
call, saving us some requests.
When the batch is executed, it will simply load the required data, fetch our previously created record objects (via cachedRecordForId
) and load the data into them. It’s all as simple as 20 lines of code.
Now comes the dreaded part. Any time someone told me to write an adapter from scratch while using Ember Data, I would feel total despair, but doing so in Ember Model is really simple, let me show you.
All of the following examples are simplified to keep things short and to the point. In a real app you would want to build the URL based on the record’s type, etc.
find: function(record, id) {
return this.ajax("/posts/" + id + ".json").then(function(data) {
Ember.run(record, record.load, id, data);
});
}
There really isn’t anything magical going on here. We get the record
and it’s id
as a parameter, do the request and load the data back into the record
, which is automatically stored in the identity map. We could implement a hook for creating a new record in similar fashion
createRecord: function(record) {
var xhr = this.ajax("/posts.json", record.toJSON(), "POST");
xhr.then(function(data) {
Ember.run(function() {
record.load(data.id, data);
record.didCreateRecord();
});
});
}
The only thing that you need to remember here is to call record.didCreateRecord()
to save the record in the identity map (among other things). We could still use this in a real app, even though I am hardcoding the URLs. Every model has it’s own adapter. You can choose to use the same one for all models, or have one model use the RESTAdapter
with a conventional API, and another one use something completely different. Which brings us to how we define our models. Let’s take a look at a very simple example
App.Post = Ember.Model.extend({
title: Ember.attr(),
content: Ember.attr()
});
App.Post.adapter = App.Adapter.create();
The only requirement for your models is that they define an adapter
property. If you were to use Ember.RESTAdapter
you would have to define a few more properties.
App.Post = Ember.Model.extend({
title: Ember.attr(),
content: Ember.attr()
});
App.Post.adapter = Ember.RESTAdapter.create();
App.Post.url = "/posts";
App.Post.rootKey = "post";
App.Post.collectionKey = "posts";
We have to define these since Ember Model doesn’t have it’s own inflector (yet). rootKey
will be used both for loading data for a single resource like /posts/1
, and when serializing the model to JSON, for example:
var post = App.Post.create({ title: "hi", content: "there" });
post.toJSON(); // => { post: { title: "hi", content: "there" } }
One of the things that people don’t like Ember Model is that it doesn’t support relationships, but the opposite is true. You can make use of the Ember.hasMany
macro when creating embedded relationships, such as the following example
App.Post = Ember.Model.extend({
comments: Ember.hasMany("App.Comment", "comments");
});
App.Post.load(1, { comments: [ { text: "ohai, Ember Model is awesome" ] });
There is no macro for relationships using ids, but you can easily implement them yourself
App.Post = Ember.Model.extend({
comment_ids: Ember.attr(),
comments: function() {
return App.Comment.find(this.get("comment_ids");
}.property("comment_ids.[]")
});
Ember Model doesn’t yet support automagic sideloading as Ember Data does, but you can easily implement it yourself in the adapter using the Model.load
method.
find: function(record, id) {
return this.ajax("/posts/" + id + ".json").then(function(data) {
Ember.run(record, record.load, id, data.post);
Ember.run(Comment, Comment.load, data.comments);
});
}
If you’re doing this in a lot of places, you might consider adding more logic to make the sideloading less ad-hoc. I’ll leave this up as an excercise for the reader. You can take a look at how Travis does this.
While Ember Model is still evolving, it is mature enough to be used in production today. I would recommend it to anyone who is starting with Ember over Ember Data, if nothing else just because it is so easy to understand. The goal of Ember Model is to stay modular and easily extensible so that you can build upon it based on your use case.
Written by Jakub Arnold of sensible.io.
We're building a tool to help businesses reach out to their customers more easily. It's called SendingBee and it's going to be awesome.
This is the blog of sensible.io, a web consultancy company providing expertise in Ruby and Javascript.