Eager Loading Polymorphic Associations in Ruby on Rails
--
For when .includes
is not enough.
Basic Rails Associations
Rails’ ActiveRecord associations permits relating one model to another. Take the example below:
class Post < ApplicationRecord
has_many :comments
endclass Comment < ApplicationRecord
belongs_to :post
end
A Post
has many Comments
. This allows us to call Post.first.comments
to get all the comments associated with a post! ActiveRecord internally generates our required SQL queries:
SELECT "posts".* FROM "posts" WHERE "posts".id = 1
SELECT "comments".* FROM "comments" WHERE "comments".post_id
N+1 Queries and .includes
However, imagine we are serializing this data to return as part of a controller for the /posts
index. In that case, instead of getting the comments for just one post, we would get it for multiple posts: Post.all.each { |p| puts p.comments }
. What happens to our SQL queries?
SELECT "posts".* FROM "posts"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 2
...
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = N
As seen above, we end up getting one query to get all the posts and then N queries to get the comments for each of the N posts. Sounds familiar? This is what is often called N+1 queries and is a major slow down in large applications.
Luckily, Rails provides a built in solution for this. Just add the .includes
method on the call to the parent object: Post.all.includes(:comments).each { |p| puts p.comments }
. This informs Rails to eager load the comments resulting in only 2 total queries:
SELECT "posts".* FROM "posts"
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, ..., N)
In case you want to preemptively detect N+1 queries in your code before it makes it into production, I recommend using the Bullet Gem that I go into more detail in this other article: