Preloading Nested Active Record Associations Syntax

Published on 2025-04-21

I recently opened a Pull Request where I was including nested associations in an Active Record query and received feedback that syntax was unfamiliar. It hadn't crossed my mind that this syntax would be unfamiliar to others so I wrote up an explanation in a comment about what was happening, the Pull Request was approved and the feature was shipped. However, that got me thinking that I would like to share the syntax and do a little exploration as to why it might have been unfamiliar to others. The query in question looked something like this.

The query syntax in question#

User.includes(team: :organization)
User.includes(team: :organization)

If this looks unfamiliar to you, let's set up the context.

class Organization < ApplicationRecord
has_many :teams
end

class Team < ApplicationRecord
belongs_to :organization
has_many :users
end

class User < ApplicationRecord
belongs_to :team
end
class Organization < ApplicationRecord
has_many :teams
end

class Team < ApplicationRecord
belongs_to :organization
has_many :users
end

class User < ApplicationRecord
belongs_to :team
end

We have three models in a three level hierarchy where Organization is at the top, Team is in the middle, and User is at the bottom. The syntax of the query is saying for all the User records we look up, we also want to load the associated Team record as well as the Organization that the Team belongs to. This loading of associated records is the resolution to the classic N+1 problem where it could be possible to hit the database multiple times as we loop through each User record. Let's look at a quick example to clarify the N+1 problem.

# N+1 problem. We query the database for the Team and Organization record on every loop
User.where(...).map do |user|
{
name: user.name,
team_name: user.team.name, # This issues a query to the database
organization_name: user.team.organization.name # This issues another query to the database
}
end

# No N+1. We have preloaded all the associated records
User.includes(team: :organization).where(...).map do |user|
{
name: user.name,
team_name: user.team.name,
organization_name: user.team.organization.name
}
end
# N+1 problem. We query the database for the Team and Organization record on every loop
User.where(...).map do |user|
{
name: user.name,
team_name: user.team.name, # This issues a query to the database
organization_name: user.team.organization.name # This issues another query to the database
}
end

# No N+1. We have preloaded all the associated records
User.includes(team: :organization).where(...).map do |user|
{
name: user.name,
team_name: user.team.name,
organization_name: user.team.organization.name
}
end

Now that we understand what the syntax is doing and why it is useful, let's explore why it might be unfamiliar.

Why is this syntax unfamiliar?#

I think the reason this syntax is unfamiliar is because it isn't mentioned in the Active Record Query Interface documentation. The documentation has many examples of using .includes and I picked out the three different types to show here.

Book.includes(:author)

Customer.includes(:orders, :reviews)

Customer.includes(orders: { books: [:supplier, :author] })
Book.includes(:author)

Customer.includes(:orders, :reviews)

Customer.includes(orders: { books: [:supplier, :author] })

Let's break down what each of these are doing.

Book.includes(:author)
Book.includes(:author)

This example showcases that a Book is related to an author via a belongs_to or has_one association.

Customer.includes(:orders, :reviews)
Customer.includes(:orders, :reviews)

This example showcases that a Customer has two relationships, has_many :orders and has_many :reviews. We're preloading both associations so that we don't have to make additional trips to the database to later load the data. Note that this isn't an N+1 example, these two relationships load lists and can be all fetched at once. This example could also be written like this.

Customer.includes([:orders, :reviews])
Customer.includes([:orders, :reviews])

The change to using array syntax is minor, but it demonstrates that associations that belong to the model can be encapsulated into an array. We see this syntax put to work in the final example.

Customer.includes(orders: { books: [:supplier, :author] })
Customer.includes(orders: { books: [:supplier, :author] })

This example builds on the previous in a more nested manner. We now see that the Order class has_many :books and each Book is related to a supplier and an author via a belongs_to or has_one association. We know that we can encapsulate associations to the same model through the array syntax and that approach is used here to preload multiple associations to Book.

Now that we understand different approaches to the preloading syntax, let's revisit the syntax of our original query and rewrite it to match the documentation.

# Original
User.includes(team: :organization)

# Rewritten to match the documentation
User.includes(team: [:organization])
# Original
User.includes(team: :organization)

# Rewritten to match the documentation
User.includes(team: [:organization])

The original query is omitting the array syntax around :organization because we're only loading a single association and that is perfectly valid syntax!

Conclusion#

I found an answer to a question on Stack Overflow, Rails - Nested includes on Active Records? from 2014 that may be the source of where I learned this syntax. The answer shows some examples of how to build up a structure of extremely nested associations which shouldn't look unfamiliar now that we've covered the basics here. Hopefully this helps the next time you're writing or reviewing some Active Record code!