HOWTO call `findBelongsTo` on the JSONAPIAdapter via Store?


#1

Does anyone have any practical examples of how to call the #findBelongsTo method on the JSONAPIAdapter?

To give some context, I have a house model which has one (1) master-bedroom.

In my master-bedroom model, I have defined the relationship to its house as such:

export default DS.Model.extend({
    ...
    house: DS.belongsTo('house')
    ...
});

I’m trying to access the master-bedroom of a house from a house route (e.g., http://localhost:4200/house/1).

I would like to use the #findBelongsTo method to do so because the URL this method generates makes the most sense to me (e.g., http://localhost:4200/house/1/master-bedroom).


#2

So in the docs it says:

Called by the store in order to fetch the JSON for the unloaded record in a belongs-to relationship that was originally specified as a URL (inside of links).

Basically (AFAIK) there are three ways to specify a related record. Option 1 is to return an id, for example if your backend returned {..., house: 3, ...} for a master-bedroom record, then when you tried to do <master-bedroom record>.get('house') Ember would basically make a findRecord('house', 3) request.

Option 2 is to specify a “links” hash on your record. This can be done from your backend, or you can hack it into the serializer so it is added when you are normalizing the records into the store. If you specify a links hash then the store will use the findBelongsTo method (this is where the quote I pulled from the docs above comes in).

Option 3 is if neither an id nor a links hash are given in the master-bedroom record, the store needs to know where to look up the data. So it uses the urlForFindBelongsTo method to construct that URL. So to customize this behavior you’d want to override urlForFindBelongsTo.

So in your case I’d say option 3 is probably what you’re looking for, but it’s hard to say.


#3

Thanks for the help!

What I was really hoping to do is that I could call something like this:

this.store.findBelongsTo('house', houseId, 'master-bedroom')

So a bit of the other way around as to what you suggested in Option 1.

FOLLOW UP QUESTION #1: Does anyone know if this invocation style/signature is possible for the #findBelongsTo method?

Option 2 seems like that would be easiest way. What I was actually using before discovering the #findBelongsTo method was using the DS.Store#queryRecord method and using the query hash.

this.store.queryRecord('master-bedroom', {houseId: houseId}

It’s a bit backwards in terms of how I originally intended to retrieve the master-bedroom record, but it would work. I would have revise the API endpoint on my backend to be able to respond to a request like this:

GET http://localhost:4200/master-bedroom?houseId=1

So some conditional logic would be necessary for the backend as the master-bedroom route would now provide at least two different types of responses: 1) it’s default response, which is an array of master-bedroom records; and now 2) a single master-bedroom record. This solution feels a bit kludgy though, or perhaps not best practice.

FOLLOW UP QUESTION #2: Does anyone know if this is best or common practice?

I don’t quite understand Option 3. Could you provide an example? It might make things more clear for me. I really don’t want to alter the URL so much. I’d like to use the conventions that ember has already prescribed, but if this is the only way to do it, then I’ll consider it.

Thanks again!


#4

I was going to avoid commenting on this thread because we don’t use jsonapi. But option 2 is how we go about things. We’re not completely consistent about where we add the links. Our original API consumer didn’t need them, Ember did, so we put the code in the serializer in ember. Lately we have been putting them in the API.

With regards to the queryparam technique, Master-bedroom?houseID=1 We do that with great regularity. The API returns an array with 1 element, not an array-less element. Keeps the consuming code sane that way.


#5

Option 3 is how you would let ember know to fetch the data at a whatever url failing options 1 and 2 (e.g. if the backend didn’t return either an house id or a links hash with a house link). Option 3 is where Ember Data would say “ok, i wasn’t given an id or a link to fetch this related data, so now I need to generate a URL that I can fetch from”.

So in your case I think you’d put something like this in your master-bedroom adapter:

import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({
  urlForFindBelongsTo(id, modelName, snapshot) {
    let baseUrl = this.buildURL(id, modelName);
    return `${baseUrl}/house`;
  }
});

These aren’t very well documented though, so I’m not 100% if modelName is the name of the model you have, or the name of the model on the relationship.

Anyway, like Chris_Lincoln we mostly use Option 2 in the cases where we need it, mostly our API uses Option 1 and returns ids for most relationships.

If you already have endpoints like “house/1/master-bedroom” or “master-bedroom/1/house” I would probably try option 2 or 3 before modifying your backend. One of the strengths of Ember Data is that the adapters and serializes can be customized to support pretty much any backend configuration, so as long as your backend is using some sort of reasonable convention it’s probably best to leave it be. Just my 2c.


#6

Thanks again for your help, @dknutsen and @Chris_Lincoln! I have a few more questions if that’s alright.

First question @dknutsen

Just so I understand Option 1 correctly, are you saying that your API returns the IDs of related models?

For example, as a JSON response to GET /house/1

house: {
  ...
  'masterBedroomId': 2, 
  'kitchenId': 5,
  ...
}  

Second Question: @dknutsen and @Chris_Lincoln

Just so I understand Option 2 correctly, are you two saying that your API returns an additional hash named links with all the IDs of related models inside a JSON response?

For example, as a JSON response to GET /house/1

house: {
  ...
  links: {'master-bedroomId': 2, 'kitchenId': 5}
  ...
}  

Third Question: @dknutsen and @Chris_Lincoln

If the relationship is a belongsTo relationship, then that would mean you would have had to query the database two extra times to find the kitchen and the master-bedroom that belongs to this house in order to populate the masterBedroomId and kitchenId fields. Is that right?


#7

First question

Yes, I believe that’s a pretty standard REST paradigm. FWIW I’m mostly using RESTAdapter/Serializer and not the JSON API versions, but I think these concepts are typically pretty similar across all of the base adapters and serializers. And actually if the payload looks like what you pasted maybe you should be using the REST adapters and serializers too. Our API returns the ids with snake case, so in our API it would look like:

house: {
  ...
  'master_bedroom_id': 2, 
  'kitchen_id': 5,
  ...
}  

At that point you’d basically have three choices:

  1. in your model, make your relationship use, for example, the key ‘kitchen_id’/‘kitchenId’ instead of ‘kitchen’
  2. munge the attribute in your serializer from ‘kitchen_id’/‘kitchenId’ to ‘kitchen’
  3. change your backend to return just ‘kitchen’ instead of ‘kitchen_id’/‘kitchenId’

Second Question

Technically our API never returns a ‘links’ hash, but Ember Data supports it, so what we do is in the serializer we add it manually like so (this is our ‘user’ serializer, and it has a hasMany(‘account’)):

  normalize: function(typeClass, hash, prop){
    // add an 'accounts' link to support our 'hasMany'
    hash.links = {
      accounts: `/users/${hash.id}/accounts`,
    };
    return this._super(typeClass, hash);
  },

So again, you could have your API return it, but as an alternative, you could just toss it in at the serializer level like we do (and it sounds like @Chris_Lincoln does the same).

Third Question

Yes, it would make three requests to fetch all of the records. When you define a relationship on a DS.Model, you have the option of specifying ‘async: true/false’. The default is true. This means that if a related record is not included in the main payload, it needs to look it up asynchronously using what we have been calling Options 1-3. If your API returned an id, it uses that to do a findRecord. If your API returned a links hash, it uses that. If your API returned none of those things it tries “Option 3”, constructing a URL by itself (which of course you can override to meet your needs). Ember is pretty intelligent about batching these requests and such, but yes it can add up to a lot of async requests.

One common paradigm is to include the FULL related record in a payload, often optionally. For example in our app if we’re fetching a ‘product’ model we have a query param called ‘embed_group’ which tells the API to embed the full ‘productgroup’ model instead of just the productgroup id. This prevents the front-end from making the multiple async requests because all the data is in the initial ‘product’ payload. How those payloads look exactly depends on your backend and how Ember Data digests them depends on what serializer you use, but if you’re worried about lots of async requests to your backend you could consider supporting embedding related records in your response payloads either as a rule or optionally.


#8

Awesome! Great information. Your third answer was also illuminating. Thanks for the help! I think that’s given me enough to go off of and start trying some things out.


#9

Excellent, good luck!


#10

I’m a little late to the party and it looks like @dknutsen has answered all your questions. In case it helps, I wrote a series of blog posts about how Ember Data loads async relationships. Check it out here: http://www.amielmartin.com/blog/2017/08/31/how-ember-data-loads-async-relationships/