Как избежать N + 1 запросов для has_many: через ассоциации - PullRequest
0 голосов
/ 10 октября 2019

Я создаю GraphQL API с Ruby on Rails и гемом graphql. Теперь у меня есть n:m отношения, например, Projects имеет много Users и Users имеет много Projects. Мои модели похожи на:

# /app/models/project.rb

has_many :project_assignments
has_many :project_managers, through: :project_assignments, source: :user
# /app/models/project_assignment.rb

belongs_to :project
belongs_to :user
# /app/models/user.rb

has_many :project_assignments
has_many :projects, through: :project_assignments

Теперь я хочу запросить все проекты и их соответствующих руководителей проектов с помощью запроса, подобного этому:

query {
  projects {
    edges {
      node {
        id        
        projectManagers {
          edges {
            node {
              id
            }
          }
        }
      }
    }
  }
}

И мои преобразователив основном похожи на Project.all, и каждый Project вызывает project.projectManagers, что приводит к сотням запросов:

  Project Load (3.3ms)  SELECT "projects".* FROM "projects"
  User Load (1.5ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 2]]
  User Load (0.7ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 3]]
  User Load (1.1ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 4]]
  User Load (1.0ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 5]]
  User Load (0.8ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 6]]
... 

Я добавил жемчужину пули, но нет никаких предупреждений об отсутствующей нетерпеливой загрузке,Фактически, если я использую Project.all.includes(:project_managers), я получаю запрос, который я хочу (SELECT "project_assignments".* FROM "project_assignments" WHERE "project_assignments"."project_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, ...)), но запросы User запускаются так или иначе:

User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 3], ["id", 2], ["id", 1]]
  User Load (0.8ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 2]]
  User Load (0.9ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 3]]
  User Load (0.8ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 4]]
  User Load (0.7ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 5]]
  User Load (0.7ms)  SELECT "users".* FROM "users" INNER JOIN "project_assignments" ON "users"."id" = "project_assignments"."user_id" WHERE "project_assignments"."project_id" = $1  [["project_id", 6]]
...

Есть ли что-нибудь, что я могу сделать предзагрузкойпользователи?

Я также пытался eager_load, но результат в основном тот же (пробовал другой пример с обычной has_many ассоциацией (нет :through):

  SQL (1.3ms)  SELECT "projects"."id" AS t0_r0, "projects"."title" AS t0_r1, "projects"."number" AS t0_r2, "projects"."description" AS t0_r3, "projects"."deadline" AS t0_r4, "projects"."archived" AS t0_r5, "projects"."customer_id" AS t0_r6, "projects"."rate_type" AS t0_r7, "projects"."daily_rate" AS t0_r8, "projects"."service_rates" AS t0_r9, "projects"."budget_type" AS t0_r10, "projects"."budget_rate" AS t0_r11, "projects"."created_at" AS t0_r12, "projects"."updated_at" AS t0_r13, "projects"."status" AS t0_r14, "projects"."slug" AS t0_r15, "project_labels"."id" AS t1_r0, "project_labels"."title" AS t1_r1, "project_labels"."description" AS t1_r2, "project_labels"."color" AS t1_r3, "project_labels"."project_id" AS t1_r4, "project_labels"."created_at" AS t1_r5, "project_labels"."updated_at" AS t1_r6 FROM "projects" LEFT OUTER JOIN "project_labels" ON "project_labels"."project_id" = "projects"."id" WHERE "projects"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8], ["id", 9], ["id", 10]]
  ProjectLabel Load (0.3ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 10]]
  ProjectLabel Load (0.5ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 2]]
  ProjectLabel Load (0.7ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 5]]
  ProjectLabel Load (0.5ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 8]]
  ProjectLabel Load (0.5ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 6]]
  ProjectLabel Load (0.6ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 4]]
  ProjectLabel Load (0.4ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 1]]
  ProjectLabel Load (0.3ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 3]]
  ProjectLabel Load (0.3ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 9]]
  ProjectLabel Load (0.5ms)  SELECT "project_labels".* FROM "project_labels" WHERE "project_labels"."project_id" = $1  [["project_id", 7]]

Ответы [ 2 ]

0 голосов
/ 11 октября 2019
Project.all.includes(:project_managers)
# Project.includes(:project_managers) # shorthand of above

... будет автоматически включать project_managers в SQL в зависимости от запроса.

Чтобы также включить project_assignment.user, просто объедините их:

Project.includes(:project_managers, project_assignments: :user)

# probably below is equivalent of above (but not sure)
# just because `project_managers` association is also going "through" :project_assignments
Project.includes(project_assignments: :user)

См. «Загрузка вложенных отношений» здесь в документах

0 голосов
/ 10 октября 2019

Вы можете делать все с помощью has_many, включая includes, что должно помочь с N + 1 запросами

has_many :project_assignments, -> { includes(:projects) }
...