In the latest Ember Data (currently 1.0.0-beta.4) saving or serializing embedded records doesn’t work the way some of us wish. By default it will serialize the parent record and include a list of IDs for the nested records. This is good practice and fine for data that will be stored in a SQL database, however, my project needs to export the entire structure into a single JSON that will be saved to a file. Even though you cannot do this out of the box, adding just a few lines of code will get it working!
Desired Behavior
For this example we’ll create a child that can have many toys. This would be a simple hasMany relationship:
App.Child = DS.Model.extend({ name: DS.attr('string'), toys: DS.hasMany('toy'), }); App.Toy = DS.Model.extend({ kind: DS.attr('string') });
If Max has a Kazoo, the serialized JSON should look like this:
child: { name: "Max", toys: [{ name: "Kazoo" }] }
However, by default Ember will simply list the Kazoo ID in the toy’s list. Which, I will state again, is excellent practice for a traditional application but doesn’t work for the case where we want everything encapsulated in a single JSON.
Overriding Your Serializer
All you need to do is override the Serializer that handles hasMany
relationships. You could have it export all relationships automatically, but this can cause some bad problems with cyclical relationships; instead make it optional with the familiar embedded:'always'
flag. Start by updating your model with the embedded flag:
App.Child = DS.Model.extend({ name: DS.attr('string'), toys: DS.hasMany('toy', {embedded: 'always'}), });
Now write your serializer:
DS.JSONSerializer.reopen({ // or DS.RESTSerializer serializeHasMany: function(record, json, relationship) { var key = relationship.key, hasManyRecords = Ember.get(record, key); // Embed hasMany relationship if records exist if (hasManyRecords && relationship.options.embedded == 'always') { json[key] = []; hasManyRecords.forEach(function(item, index){ json[key].push(item.serialize()); }); } // Fallback to default serialization behavior else { return this._super(record, json, relationship); } } });
Now when you serialize a child object, all the nested toy objects will be included.
Add belongsTo Relationships
This handles the hasMany
relationships, but we’ll probably also want to support belongsTo
. For example, what if each toy had dimensions:
App.Toy = DS.Model.extend({ kind: DS.attr('string'), size: DS.belongsTo('size', {embedded: 'always'}) }); App.Size = DS.Model.extend({ height: DS.attr('number'), width: DS.attr('number'), depth: DS.attr('number') });
To support this, we do the same thing as before but with the serializeBelongsTo
method:
serializeBelongsTo: function(record, json, relationship) { var key = relationship.key, belongsToRecord = Ember.get(record, key); if (relationship.options.embedded === 'always') { json[key] = belongsToRecord.serialize(); } else { return this._super(record, json, relationship); } }
Now when you serialize a child object, the resulting JSON will look something like this:
child: { name: "Herbert", toys:[{ kind: "Kazoo", size: { height: 5, width: 5, depth: 10 } }] }
Demo
I put this example up on JSFiddle so you can see and play with it: http://jsfiddle.net/jgillick/LNXyp/9/
Beware of Cyclical Relationships!
One problem you might run into is when two objects point at eachother. Take this example from the Ember site:
App.Post = DS.Model.extend({ comments: DS.hasMany('comment') }); App.Comment = DS.Model.extend({ post: DS.belongsTo('post') });
If you marked both of these relationships as embedded:'always'
, it would create an infinite loop of serialization. A post would include comments which would include the post and so on.
In this case, it’s best to decide which direction you want your object to nest and only put the embedded option on one of them. For example:
App.Post = DS.Model.extend({ comments: DS.hasMany('comment', {embedded: 'always'}) }); App.Comment = DS.Model.extend({ post: DS.belongsTo('post') });