Search code examples
ruby-on-railsactiverecordrails-api

How to fix this n+1 query: Limiting child association to most recent record in JSON api response


I am trying to return a list of parent records, along with each parents most recent child record.

In my controller I have:

def index
  projects = current_user.projects.includes(:tasks)

  render json: projects.as_json(
    methods: [:most_recent_task],
  ), status: 200
end

The method most_recent_task uses an approach articulated here and summarized below:

class Task < ApplicationRecord
  class << self
    def in_order
      order(created_at: :asc)
    end

    def recent(n)
      in_order.endmost(n)
    end

    def endmost(n)
      all.only(:order).from(all.reverse_order.limit(n), table_name)
    end
  end
end

and

class Project < ApplicationRecord
  has_many :tasks

  def most_recent_task
    tasks.recent(1)[0]
  end
end

This approach returns the correct JSON response, but I am now obviously dealing with n+1 queries for Task on every request.

I've tried chaining scopes, using :includes and :limit, but can't seem to crack this nut. Perhaps using a JSON serializer could solve it? But I am trying to avoid that extra dependency for now. Any advice?


Solution

  • One solution is to define a has_one with an association scope:

    has_one :most_recent_task, -> { order(created_at: :asc) }, class_name: "Task"
    

    You can then use includes to eagerly load the data:

    >> Project.includes(:most_recent_task).all
      Project Load (0.3ms)  SELECT  "projects".* FROM "projects" LIMIT $1  [["LIMIT", 11]]
      Task Load (0.5ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" IN (1, 2) ORDER BY "tasks"."created_at" ASC
    

    Note that it's querying all tasks for each project, not just the most recent one. But there's no N+1, and Project#most_recent_task is nicely expressive.