Skip to content

n+1 for side loaded records #83

@adambedford

Description

@adambedford

I'm using JSON API include to side load two associations (two levels deep: company,company.company-master) and I'm seeing an N+1 issue when plugging in Pundit + JSONAPI Athorization into my JSONAPI Resources Processor stack.

I've tried explicitly including those two associations in the policy scope with no success, although this shouldn't be required since JSONAPI Resources should be handling this. It does when JSONAPI Authorization isn't involved.

The logs produced:

Started GET "/api/v1/job-applications?include=company%2Ccompany.company-master" for ::1 at 2017-09-20 18:11:06 -0700
[NAME COLLISION] `type` is a reserved key in EventResource.
[NAME COLLISION] `type` is a reserved key in AccountResource.
Processing by Api::V1::JobApplicationsController#index as JSON
  Parameters: {"include"=>"company,company.company-master"}
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 6], ["LIMIT", 1]]
  JobApplication Load (1.4ms)  SELECT  "job_applications".* FROM "job_applications" WHERE "job_applications"."user_id" = 6 ORDER BY "job_applications"."id" ASC LIMIT $1 OFFSET $2  [["LIMIT", 1000], ["OFFSET", 0]]
  Events::Applied Load (1.6ms)  SELECT "events".* FROM "events" WHERE "events"."type" IN ('Events::Applied') AND "events"."subject_type" = $1 AND "events"."subject_id" IN (465, 466, 467, 468, 479)  [["subject_type", "JobApplication"]]
  Source Load (0.7ms)  SELECT "sources".* FROM "sources" WHERE "sources"."id" = 21
   (1.8ms)  SELECT "job_applications"."id", "companies"."id" FROM "job_applications" INNER JOIN "companies" ON "companies"."id" = "job_applications"."company_id" LEFT OUTER JOIN "events" ON "events"."subject_id" = "job_applications"."id" AND "events"."type" IN ('Events::Applied') AND "events"."subject_type" = $1 LEFT OUTER JOIN "sources" ON "sources"."id" = "job_applications"."source_id" WHERE "job_applications"."user_id" = 6 AND "job_applications"."id" IN (465, 466, 467, 468, 479)  [["subject_type", "JobApplication"]]
  Company Load (1.0ms)  SELECT "companies".* FROM "companies" WHERE "companies"."user_id" = 6 AND "companies"."id" IN (260, 285, 286, 287, 288)
  CompanyMaster Load (1.3ms)  SELECT "company_masters".* FROM "company_masters" WHERE "company_masters"."id" IN (338, 312, 79, 301, 359)
   (1.8ms)  SELECT "job_applications"."id", "companies"."id", "company_masters"."id" FROM "job_applications" INNER JOIN "companies" ON "companies"."id" = "job_applications"."company_id" INNER JOIN "company_masters" ON "company_masters"."id" = "companies"."company_master_id" LEFT OUTER JOIN "events" ON "events"."subject_id" = "job_applications"."id" AND "events"."type" IN ('Events::Applied') AND "events"."subject_type" = $1 LEFT OUTER JOIN "sources" ON "sources"."id" = "job_applications"."source_id" WHERE "job_applications"."user_id" = 6 AND "job_applications"."id" IN (465, 466, 467, 468, 479)  [["subject_type", "JobApplication"]]
  CompanyMaster Load (1.7ms)  SELECT "company_masters".* FROM "company_masters" WHERE "company_masters"."id" IN (338, 312, 79, 301, 359)
   (0.6ms)  SELECT COUNT(*) FROM "job_applications" WHERE "job_applications"."user_id" = 6
  Company Load (0.6ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."id" = $1 LIMIT $2  [["id", 287], ["LIMIT", 1]]
  CompanyMaster Load (0.5ms)  SELECT  "company_masters".* FROM "company_masters" WHERE "company_masters"."id" = $1 LIMIT $2  [["id", 301], ["LIMIT", 1]]
  Company Load (0.9ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."id" = $1 LIMIT $2  [["id", 288], ["LIMIT", 1]]
  CompanyMaster Load (3.7ms)  SELECT  "company_masters".* FROM "company_masters" WHERE "company_masters"."id" = $1 LIMIT $2  [["id", 359], ["LIMIT", 1]]
  Company Load (0.6ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."id" = $1 LIMIT $2  [["id", 286], ["LIMIT", 1]]
  CompanyMaster Load (2.6ms)  SELECT  "company_masters".* FROM "company_masters" WHERE "company_masters"."id" = $1 LIMIT $2  [["id", 79], ["LIMIT", 1]]
  Company Load (0.6ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."id" = $1 LIMIT $2  [["id", 285], ["LIMIT", 1]]
  CompanyMaster Load (0.7ms)  SELECT  "company_masters".* FROM "company_masters" WHERE "company_masters"."id" = $1 LIMIT $2  [["id", 312], ["LIMIT", 1]]
  Company Load (0.5ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."id" = $1 LIMIT $2  [["id", 260], ["LIMIT", 1]]
  CompanyMaster Load (0.6ms)  SELECT  "company_masters".* FROM "company_masters" WHERE "company_masters"."id" = $1 LIMIT $2  [["id", 338], ["LIMIT", 1]]
  Rendering text template
  Rendered text template (0.4ms)
Completed 200 OK in 576ms (Views: 1.7ms | ActiveRecord: 45.0ms)

As you can see, the side loaded records are fetched as normal. However, after that, individual queries are performed, which shouldn't be happening.

This is my policy:

class JobApplicationPolicy < ApplicationPolicy
  def index?
    current_user.has_role?(:jobseeker)
  end
  alias_method :new?, :index?
  alias_method :edit?, :index?
  alias_method :create?, :index?
  alias_method :update?, :index?
  alias_method :destroy?, :index?

  class Scope < Scope
    def resolve
      # Applied Event is included because model delegates `date_applied` to `applied_event`
      new_scope = scope.includes(:applied_event, :source)

      if current_user.has_role?(:admin) || current_user.has_role?(:advisor)
        new_scope.joins(user: [:account]).where(accounts: { id: current_user.account.id })
      else
        new_scope.where(user: current_user)
      end
    end
  end
end

The scope returned should already include the includes(company: [:company_master]), and explicitly adding it doesn't resolve the issue.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions