I'm building a GraphQL API with Ruby on Rails and the graphql gem. Now I have some n:m
relations, like Projects
have many Users
and Users
have many Projects
. My models are like:
# /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
Now I want to query all projects and their corresponding project managers with a query like this:
query {
projects {
edges {
node {
id
projectManagers {
edges {
node {
id
}
}
}
}
}
}
}
And my resolvers are basically like Project.all
and each Project
calls project.projectManagers
, which results in hundreds of queries:
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]]
...
I've added the bullet gem, but there is no warning about a missing eager loading. In fact, If I use Project.all.includes(:project_managers)
, I get the query that I want (SELECT "project_assignments".* FROM "project_assignments" WHERE "project_assignments"."project_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, ...)
), but the User
queries fires anyhow:
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]]
...
Is there anything that I can do the preload the users?
I tried eager_load
also, but the result is basically the same (tried another example with an normal has_many
association (no :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]]
You can do all sorts of things with has_many
including includes
which should help with N+1 queries
has_many :project_assignments, -> { includes(:projects) }
Project.all.includes(:project_managers)
# Project.includes(:project_managers) # shorthand of above
... would include project_managers
automatically in the SQL depending on the query.
To also include project_assignment.user
, then just merge them:
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)
See "Loading nested relationships" here in the docs
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With