Two-way symmetric relationships in Ember with JSON API - Part 2

29 November 2016

In Part 1 of this series, we saw what symmetric relationships are, how to model them on the back-end (using Rails) and how to implement a simple app that displays such relationships in Ember.js, adhering to the JSON API specification for serializing data.

The solution we came up with worked but was a very naive one. For N relationships a resource had, it made 2N+1 queries to the back-end to display them. We should do better and in this post we'll see how.

(If you haven't read the previous post, this one will not make much sense, so I recommend to at least skim it before you delve into this one.)

One request to rule them all

We'd observed that for each person whose friendships we wanted to observe, our application made 2N+1 requests. One to grab the friendships themselves, and then two requests for each friendship in that collection, one to fetch the friender, one to fetch the friended person.

Too many XHRs

We also noticed that those people (instances of the Person model) had already been fetched at that point so the extra requests were for nothing. Our mission is thus to reduce the 2N+1 requests to a single one, the one that fetches the friendships.

How would Ember (Data) know?

If we take a look at the data returned by the request for friendships, you can see that both the friended and friender end of each are just links with no type or identity information (like /friendships/1/friended). This is all that's needed for an asynchronous relationship, since the client (Ember Data, in this case) can just fetch the relationship data when/if it needs it.

No linkage data

The solution, then, might be to include some data about the resource that the relationship refers to. In the JSON API vocabulary this is called resource linkage:

Resource linkage in a compound document allows a client to link together all of the included resource objects without having to GET any URLs via links.

Digging around in the jsonapi-resources source, we find a relationship option called always_include_linkage_data that seems to do what we need. Let's add that to the relationships of the friendship resource and see:

1# app/resources/friendship_resource.rb
2class FriendshipResource < JSONAPI::Resource
3  has_one :friender, always_include_linkage_data: true
4  has_one :friended, always_include_linkage_data: true
5  attributes :strength
6end

If we now reload our Ember app, we see how a data key was added to each relationship in the response, uniquely identifying the person resource that is the friender (or friended) in the friendship relationship:

Relationship with linkage data

Furthermore, the extra XHRs we wanted to eliminate are now indeed gone as Ember Data is smart enough to just use the referred resources that are already in the store:

Just the XHRs we need

Let's just be friends (not friendeds or frienders)

We have now achieved what we'd wanted and only have to make one request per person to fetch and display their friendships.

It looks a bit weird, though, that when a person's friendships are displayed, we also display the person's name, too:

Mike McCready's friendships - Part 1

Let's fix that by transforming the friendships of the person to an array where each item only contains the friend's name (and the strength of the friendship):

 1// app/controllers/people/show.js
 2import Ember from 'ember';
 3
 4const { Controller, computed } = Ember;
 5
 6export default Controller.extend({
 7  friendships: computed('model.friendships.[]', function() {
 8    let person = this.get('model');
 9    let friendships = this.get('model.friendships');
10    return friendships.map((friendship) => {
11      let friend;
12      if (friendship.get('friended.id') === person.get('id')) {
13        friend = friendship.get('friender');
14      } else {
15        friend = friendship.get('friended');
16      }
17      return {
18        friend,
19        strength: friendship.get('strength')
20      };
21    });
22  })
23});

Nothing fancy going on, we check which end of the relationship the person in question (the model) is, and then only return the other end.

We should now use friendships in the template instead of model.friendships:

 1<div class="panel panel-default">
 2  <div class="panel-heading">
 3    <h3 class="panel-title">Friends of {{model.name}}</h3>
 4  </div>
 5  <div class="panel-body">
 6    {{#if friendships.length}}
 7      <ul class="friend-list">
 8        {{#each friendships as |friendship|}}
 9          <li class="friend-list-item">
10            <span class="name">{{friendship.friend.name}}</span>
11            <span class="badge">{{friendship.strength}}</span>
12          </li>
13        {{/each}}
14      </ul>
15    {{else}}
16      <div class="empty-list">
17        <p class="empty-message">{{model.name}} has no friends :(</p>
18      </div>
19    {{/if}}
20  </div>
21</div>

It works, we indeed only see the friend's name, not the person's:

Relationship with linkage data

Resources

Hopefully you can now implement a symmetric relationship with relative ease, the next time you encounter it.

I made the source code of both the Ember app and the Rails API available on Github. If you want to learn more about the jsonapi-resources gem, I suggest you visit the documentation site.

Finally, if you'd like to receive the series as a pdf, fill out the form below and I'll send it to you right away!

Share on Twitter