17

Background

I have a rails application with deeply nested associations.

                          .-< WorkPeriod
Timecard -< Week -< Day -<--< Subtotal
                          `-< Adjustment

-<  (has many)

I'm using Active Model Serializer to build out the API.

On the client side I want to load a timecard and all it's associations in one shot.

Currently my serializers look like this,

class TimecardSerializer < ActiveModel::Serializer
  embed :ids, include: true
  has_many :weeks
end
class WeekSerializer < ActiveModel::Serializer
  embed :ids, include: true
  has_many :days
end
# ... etc ...

Problem

This all works find, except nothing gets eager-loaded. So it ends up making lots of calls to the database for each request. For each week, it makes a separate request for the days in that week. And for each day, it makes a separate request for it's work_periods, subtotals, and adjustments.

alotofnoodles
  • 1,093
  • 10
  • 12
Ryan
  • 9,340
  • 5
  • 39
  • 42

2 Answers2

13

One solution is to define your own weeks method on the TimecardSerializer. From there you can .includes() all the associations you want to eager load.

class TimecardSerializer < ActiveModel::Serializer
  embed :ids, include: true
  has_many :weeks

  def weeks
    object.weeks.includes(days: [:sub_totals, :work_periods, :adjustments])
  end
end

All the queries will still show up in the log but most will be a cached query instead of a real one.

Ryan
  • 9,340
  • 5
  • 39
  • 42
  • 2
    This is fine for loading a single resource, but won't work if this serializer is used to return a list of `Timecard`s. In particular, the `weeks` won't be eagerly loaded, although the `weeks`' associations will be. – hjdivad Aug 12 '15 at 23:42
9

I had a similar issue. I fixed it in my controller. I like the idea of putting it in the serializer, but having it in the controller catches the n+1 weeks problem created by the ArraySerializer too.

Timecard.find(params[:id]).includes(weeks: [{ days: [:sub_totals, :work_periods, :adjustments] }])

and

Timecard.includes(weeks: [{ days: [:sub_totals, :work_periods, :adjustments] }])

should now eager load and limit the query to just six db hits.

James
  • 559
  • 4
  • 16