Article summary
Side-loading is an efficient way for a developer to pull multiple pieces of relevant JSON data (i.e. data for multiple model types) from a single HTTP request in a client-server implementation. Rather than requiring a client to make multiple requests to fetch the full set of relevant data, side-loading automatically sends all relevant data back from the server.
For example, if you were implementing a web page to display all blog posts and their comments, you would need to fetch all the BlogPost
and Comment
records. By side-loading the Comment
data along with the BlogPost
data, all relevant data can be received with one single request. The side-loaded data might look something like this example, which shows two posts and three total comments, each associated to a particular post:
{
"posts": [
{ "id": 1,
"title": "Side-load Like a Pro With Ember-RESTless",
"body": "...",
"author": "Matt Rozema"
}, {
"id": 2,
"title": "Another really good blog post",
"body": "...",
"author": "John Doe"
}
],
"comments": [
{ "id": 1, "author": "John Doe", "comment": "Amazing post!", "post_id": 1 },
{ "id": 2, "author": "Jane Doe", "comment": "Life Changing!", "post_id": 1 },
{ "id": 3, "author": "Mrs. Doe", "comment": "You rock!", "post_id": 2 }
]
}
On my current project, we are building a web application using Ember.js and a persistence layer called Ember-RESTless. Ember-RESTless is a lightweight alternative to the ever-so-popular Ember Data. We chose Ember-RESTless for this project because it doesn’t require full-blown Ember Data support, and also due to Ember Data’s instability at the time of project kickoff. Overall, it was a good choice, and working with it has been relatively painless compared to some of the alternatives. However, one of the challenges of using Ember-RESTless is a general lack of support for side-loading records.
The Problem
Ember-RESTless does provide a few small helpers for side-loading. The loadMany
and load
APIs take in raw JSON and load it into an Ember Object, which can then be set onto the model. However, the problem is:
- We usually don’t have direct access to the raw JSON.
- Even if we did, it still requires manual and non-pragmatic code (DRY, anyone?) to load the side-loaded data onto the model.
These problems are enough to discourage a lot of developers from side-loading, or to make them search for another persistence layer. In fact, one of my coworkers commented on our Ember channel in Slack earlier this year:
“I’ve realized that Ember-RESTLess doesn’t actually support side-loading particularly well, so I may go on a hunt for another data persistence framework…again.”
Hark, hunt no more! Rather than shy away from using side-loading in Ember-RESTless, my team and I decided to build our own side-loading module.
The Goods
Our solution was to create a class that defines a loadSingle
and loadMany
API, which wraps the loadMany
Ember-RESTless API. Based on the structure of the model, it will determine the data relationships between the resource class (the model being directly fetched) and the side-loaded keys. Once it determines the relevant side-loaded records (models) based on ID(s), it loads the side-loaded data onto the resource class’ model, so all relevant data is available to the view and controller.
Below is the definition of the loadMany
API from the SideLoader
class. I placed numerous comments to describe the algorithm.
loadMany: function(resourceClass) {
// GET the items from the 'resourceClass' endpoint
return resourceClass.fetch().then(items => {
var sideLoad = {};
var mainKey = pluralize(resourceClass.resourceName);
// Dig into the depths of Ember-RESTless to find the raw JSON
// returned from this request
var rawData = items.currentRequest._result;
// Create a hash of Ember Objects for each piece of side-loaded
// data, using the Ember-RESTless loadMany() API...
Object.keys(rawData).forEach(key => {
if(key !== mainKey) {
// Find the model of the side-loaded record
var sideClass = RL.client.adapter.serializer
.modelFor(singularize(key.dasherize()));
sideLoad[key] = sideClass.loadMany(rawData[key]);
}
});
// For each record (item) in the resourceClass and for each
// type of side-loaded record, determine the relationship and
// set the relevant side-loaded records onto the main object
items.forEach(item => {
Object.keys(sideLoad).forEach(sideKey => {
// We expect the convention of dependent keys to have the name:
// 'attributeId' or 'attributeIds', and 'attribute' would be the
// JSON key.
var sideRecords = sideLoad[sideKey];
var camelKey = singularize(sideKey).camelize();
var idKey = camelKey + "Ids";
var foreignKey = resourceClass.resourceName.camelize() + "Id";
// Test for "has and belongs to many" or "belongs to":
// If the main record contains the ID of one of the side-loaded
// records, then it is "has and belongs to many" or "belongs to"
if(item.get(idKey)) {
item.set(sideKey.camelize(), sideRecords.filter(sideRec => {
return item.get(idKey).contains(sideRec.get('id'));
}));
}
// Test for "has many":
// If the side-record contains the foreign key, then it is a "has many"
else if (sideRecords.get('firstObject').get(foreignKey)) {
item.set(sideKey.camelize(), sideRecords.filter(sideRec => {
return sideRec.get(foreignKey) === item.get('id');
}));
}
});
});
return items;
});
}
With this in place, and some glue code to export a SideLoader
object, a developer can now simply side-load data with the following model hook:
import BlogPost from '../../models/blog-post';
export default Ember.Route.extend({
model: function() {
return SideLoader.loadMany(BlogPost);
}
});
and the following model definitions:
var BlogPost = RL.Model.extend({
title: RL.attr(),
body: RL.attr(),
author: RL.attr()
});
var Comment = RL.Model.extend({
author: RL.attr(),
comment: RL.attr(),
postId: RL.attr()
});
export default BlogPost;
export default Comment;
This solution will fetch all blog posts and also automatically load the side-loaded comments for each blog post (i.e. a BlogPost
object will have a comments
property containing an array of comments)!
You can see the full code for loadMany
and loadSingle
APIs here.
Have you ever faced a similar problem in Ember-RESTless (or any other persistence layer)? How did you solve it?