Avoiding n+1 query on association with async: true

I have a rails backend using JSON:Serializer and I’m running into a performance issue.

I have a 1:n relation between generators and items. There are a few thousand items per generator. And an index call in rails looks like this:

// controller #index and #show
GeneratorSerializer.new(query, { include: %w[items] }).serializable_hash

// serializer
class GeneratorSerializer
   . . . 
  has_many :items // a few dozen to a few thousand
end
  
// ember generator model
export default class GeneratorModel extends Model {
  . . .
  @hasMany('item', { inverse: 'generator' }) items;
}

PROS rendering each generator “show” page is instant and client-side rendering of a page of items is fast

CONS the generator “index” page is slow (2-3 seconds) as each generator queries its items even if not needed

Option 2:

// controller #index
GeneratorSerializer.new(query).serializable_hash // not including items by default here

// controller #show
GeneratorSerializer.new(model, { include: %w[items] }).serializable_has // am including items here 

// serializer
class GeneratorSerializer
   . . . 
  has_many :items // a few dozen to a few thousand
end
  
// ember generator model
export default class GeneratorModel extends Model {
  . . .
  @hasMany('item', { async: true, inverse: 'generator' }) items;
}

PROS The index page for generators is now super fast (of course) since each one is not querying the items

CONS Rendering the generator is super slow as the call to @model.items seems to result in thousands of queries to the #show endpoint.

So the question, is there a out-of-the-box way to say for an {async: true} association to use the #index backend endpoint? Or, alternatively, refresh the parent model with a call to its #show endpoint where the asociated models can be included just for that endpoint?

There are a couple things you can do to help but long story short hasMany relationships, in their current form, just aren’t usable past a certain size. (Sidebar: Ember Data is about to undergo some very big changes so this situation will improve a lot). Same goes with store.findAll. In the app I work on (large scale rails app) we’ve basically stopped using hasMany relationships and just query for everything instead.

That said there are some things you can do to help. The first and easiest, if your backend supports it, is to turn on “coalescing”. This will request from the index route with an ids filter instead of making hundreds of individual detail route requests. That’s still slow though and you’ll still hammer your backend with work that it doesn’t necessarily need to do.

You could also use “links” relationships instead of serializing ids for your hasMany relationships. This gives you more control over how the relationship data is requested.

Another option is to use a synchronous relationship and manually fetch the related records you want along side the primary. Some have argued that sync relationships should have been the Ember Data default and I agree. They’re a little more work but sidestep some big issues.

Lastly you could consider just not using a hasMany relationship, and forcing pagination on any large set of records. This is usually a change to design but at scale it’s necessary. Even serializing a large set of ids in your Rails serializer can be prohibitively slow. Then you can hit issues with fetching and rendering all the data because it increases memory load, request bottlenecks, and render time. For example in our app we force pagination for everything and we’ve built up some nice primitives components/data fetching constructs so fetching related records is easy even without the hasMany defined. If you build out a little code to support your relationship data patterns in your app I think you’ll find that hasMany relationships aren’t very useful. Which brings me back to what I was saying earlier. The findAll store method, and hasMany relationships only make sense in fairly small data graphs. They’re great and super easy if you’re building out a blog app or something simple but once you hit a certain point they become untenable and you need to use different patterns anyway.

1 Like

Thank you, once again, for your in depth response! I had forgotten about coalesce. I’m going to give that a shot, short term. In the long term, I think I’m going to follow your advice and remove the explicit hasMany.

1 Like

Just to follow up, coalesceFindRequests worked pretty well. I think I will refactor in the future, but this gets me past my current performance issue. For reference it ends up looking like:

export default class ApplicationAdapter extends JSONAPIAdapter {
  namespace = 'api/v1'
  coalesceFindRequests = true
}

And the API request looks something like:

GET api/v1/items?filter%5Bid%5D=123%2C124%2C....

Awesome! Glad you got it working. Coalesce definitely helps a ton with requests. Our app would probably crash immediately if we didn’t use it :joy:

Even if you avoid hasMany relationships you’ll usually want to coalesce. For example if you’re rendering a table or list backed by query, and each row renders a belongsTo you still have the same issue so coalescing helps in those situations also.

Another thing that can help is using includes to serialize related data in a single request. This doesn’t really help the scaling problem but it can trim down the number of requests you need to make by a significant margin even with coalescing factored in.

Anyway, glad you found something that works for you, good luck!

1 Like