I had an opportunity to write an app with deeply nested relationships between models in the backend, and in this post I’ll give some examples showing why JSON API is the best choice for such an app.
I’ve been using Ember for almost a year now. I like the convention over configuration approach - it makes it easy to explore the possibilities that using Ember and Rails together creates. I had an opportunity to write an app with deeply nested relationships between models in the backend, and in this post I’ll give some examples showing why JSON API is the best choice for such an app. JSON API is quite a new standard, but I have experience with both ActiveModelAdapter and JSON API and cannot stress enough that JSON API is much easier to work with.
Side loading vs not side loading
First of all, you should think about sideloading your relationships. What does sideloading mean? Let’s say you have two models with serializers as follows:
The example JSON response without sideloading (or, say, including) for
user will look like this in the users controller:
With payload as follows:
The Ember data serializer will deserialize this into a
User record with the possibility of automatically fetching the related notes, if necessary (e.g. explicitly in the template
). However, this means making a request to the API for each note, which, if you have a huge list, can end up being quite cumbersome. Therefore, we can make our serializers use sideloading and modify the controller as follows:
This way, we can include these objects in one response payload. Ember data will notice that the payload contains additional hashes for other models and will automatically create instances for sent records. For example:
This way, we can decrease number of requests to our API and improve the user experience. You can even include nested relationships like
includes: %w(notes notes.comments). This is not possible while using Active Model Serializers 0.8 without JSON API - all includes are defined in the serializers, which is a maintenance nightmare when you have a different serializer for each endpoint.
Moreover, JSON API handles the
type key for each relationship. Thanks to this, you don’t have to worry if your relationship, which is named
friends, but addresses
has_many :friends, class: ‘User’, foreign_key: ‘user_id’) will be properly deserialized into the
friends relationship in Ember. Last, but not least, there is basically no difference between polymorphic and non-polymorphic relationships - both store the
However, serializers traverse your object's relationships with a kind of
each method. Make sure you properly include relationships using the
eager_load methods. Without it, you will end up with lengthy N+1 queries. However, there is no panacea for such problems and unpleasant situations will inevitably crop up.
Handling deeply nested relationships
Let's say that you have a more interesting scenario in your application. It's not rare to have tens of models in your app, all connected by relationships. Let's say you have a company that has many requests, these requests have orders, notes, customers, etc. - tens (or even hundreds) of records. Even with some kind of AJAX pagination, fetching a couple of requests with all that deeply nested data can significantly worsen the user’s experience by making them wait.
On the other hand, it's very rare to actually need all of this data at once - you likely only need the count of orders in one request or just a customer name that belongs to that request - not the deeply nested related objects. To improve performance, you can create two (or more) types of serializers - the base one and the "side-loaded one". In your
show endpoints you use full ones, (it’s probably worth it to have sideloaded the full relationships), but in
index endpoints you can use the thin ones.
Let’s say you have relationships as follows:
Request index view, you do not need those
LineItems at all, so simply don’t serialize them:
This way, entering your
Requests index route will wait for the Requests payload sideloading only for the first level of the deep relationships and won’t make any additional database queries to get each order line item’s ids. On the other hand, this solution will eventually lead to cached models without sideloaded records. If you enter the
order route from the above example, your model hook may resolve without sideloaded relationships, which is wrong, as they exist but were not fetched. After recent changes in ember-data, if you need to fetch sideloaded order records in your model hook using
store.findRecord, it will immediately resolve with the cached thin order (from sideload).
In the background, ember-data will hit your
orders endpoint to get the latest data and if you respond with the sideloaded
lineItems relationship it will eventually get rerendered. You can also explicitly make ember-data reload models from endpoints without resolving immediately, thus making sure that, in your controller, you will have both order and its line items at the same time, with a simple
Take a look at this thorough blog post about ember-data 1.13 release.
In this post, I tried to show you how easy it can be to work with Rails API when you adhere to the conventions and standards provided by JSON API. I have been working with default ActiveModelSerializers adapters since version 0.8 (which didn’t support JSON API) and I think that it’s currently much easier and flexible. Don’t forget about the possibility of explicitly sideloading individual models in each endpoint. It was not so easy before!
With this knowledge, you can build very complex applications while still having a balance between ease of maintenance and performance. However, I am pretty sure that you’ve come across (or will find) more complex examples. Would you care to share them with us? We’d love to hear about them in the comments!