Two-way symmetric relationships in Ember with JSON API - Part 1
17 November 2016
Definition
In data modelling, a symmetric relationship is a special kind of relationship where the description of the relationship from the perspective of one end of the relationship is identical to looking at it from the perspective of the other end.
Friendship between people is a good example. If Megan is Selma's friend, it follows that Selma is Megan's friend, too. On the other hand, the "knows" relationship between two people is not a symmetric one. I might know Danny Carey (the drummer of Tool), but that does not imply he knows me.
Historical background
My research into how to model and implement such a relationship in an Ember application was sparked by this Stack Overflow question that was posed by a reader of my book. It was more difficult than I thought it would be so I was intrigued to find the (an) answer.
My solution turned out to have a fairly large API component, too, so the following post will show both the server-side implementation (in Rails) and the client-side one (in Ember).
If you don't speak Rails, fear not. The code is straightforward and easy to understand without any substantial Rails knowledge, thanks in most part to the gem that makes it extremely easy to serialize data models and relationships to json:api format, jsonapi-resources.
Data modelling
We'll start with the data modelling part, which is the Rails side.
To be able to model our problem in the data layer, let's say that Friendships have a friender
and a friended
end of the relationship and a strength attribute that measures how strong their friendship is.
We should create a (data) migration that will create a database table when run:
1 $ rails g migration create_friendships
Let's fill in the generated migration with the above attributes:
A Friendship, then, is between two people (Persons), so let's define that in the corresponding model file:
We'll want to list all the friendships a person has so a friendships
method needs to be added to the Person class:
We select the friendships where either the friender
or the friended
is the person we query it for. This is where the symmetric aspect of the relationship is implemented. We don't care if the person friended somebody or if that somebody friended him, they are friends.
Note that modelling it this way, we could split up the symmetric relationship into the two constituent parts. We could return only the friendships where the person in question "initiated" it (was the friender), or "let himself be friended" (was the friender).
Server endpoints, resources, serializing relationships
We could now turn our attention to setting up the endpoints and serializing the model, and relationship data for the client application to consume. First, let's install the jsonapi-resources gem:
1 $ gem install jsonapi-resources
This gives us a jsonapi:resource generator that we can use to create both the endpoints and the serializer for our resources.
The created resources are placed in the app/resources
folder. Let's add the attributes we want to serialize for each one:
Creating the endpoints is no more work than adding a jsonapi_resources
call for each resource in the router configuration:
The gem also provides a controller generator so let's use it to create controllers for our resources:
They can be left empty but they need to be created in a way that they are descendants of JSONAPI::ResourceController (the generator takes care of that):
The back-end is now done, we can switch our focus to the Ember app.
The front-end
We want a list of people (rock stars, of course) and then have a list of their friendships on the person details page.
The first step is to set up the routes:
The model hooks for these routes are the classic, "fetch'em all" and "fetch the one that matches the id" methods of Ember Data's store:
Before we move on to writing the templates, let's define the models:
1// app/models/person.js 2import DS from 'ember-data'; 3 4const { Model, attr, hasMany } = DS; 5 6export default Model.extend({ 7 name: attr(), 8 friendships: hasMany(), 9 frienderFriendships: hasMany('friendship', { inverse: 'friender' }), 10 friendedFriendships: hasMany('friendship', { inverse: 'friended' }), 11});
1// app/models/friendship.js 2import DS from 'ember-data'; 3 4const { Model, attr, belongsTo } = DS; 5 6export default Model.extend({ 7 strength: attr('number'), 8 friender: belongsTo('person', { inverse: 'frienderFriendships' }), 9 friended: belongsTo('person', { inverse: 'friendedFriendships' }), 10});
This is rather standard Ember Data stuff, possibly with the exception of the inverse
definitions. Since we have two relationships between Person
and Friendship
we need to specify the other end of each relationship and that's what we do with the inverse
option.
With the models and routes in place, we can now see what the templates should look like.
The top-level people
route is again fairly straightforward:
The each
loop iterates through each person and renders a link for each of those that will take us to the person details page, which will display the person's friendships.
Listing a person's friendships
There is nothing fancy going on here, either. The model
is the person retrieved in the route. For each friendship that he has, the friender's and the friended's name are rendered along with the strength of the relationship. (Either friender
or friended
will be the person itself, but we can ignore that in the first version.)
This naive approach works, the friendships for the selected person are listed correctly:
A 2N+1 problem
However, looking at the requests to the backend for just one page, one gets the impression that we're not done yet:
For each friendship the person has, two requests are sent to the backend. One to fetch the friender
and another one to fetch the friended
person. This is not an N+1 query problem, this is worse, a 2N+1 query problem!
On top of that, those requests are sent for no good reason as we'd previously loaded the people referred by those friended
and friended
relationships.
In the next part, we'll see how these wasteful requests can be eliminated and we'll also make the person details page less perplexing by not displaying the person whose page we're looking at in the relationships. Stay tuned!
UPDATE: Part 2 is now available!
Share on Twitter