Last Updated: February 25, 2016
·
13.32K
· widescape

Mind the order of :includes for has_many :through

I was looking through my Rails 3.2.13 code to reduce database queries when I found something very odd.

# user.rb
class User < ActiveRecord::Base
  has_many :attendances
  has_many :events, :through => :attendances
end

I fetched a user and then wanted to check if there were events:

u = User.includes([:attendances, :events]).find(1) # => #<User>
User Load (0.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Attendance Load (0.9ms)  SELECT "attendances".* FROM "attendances" WHERE "attendances"."user_id" IN (1)
Event Load (0.7ms)  SELECT "events".* FROM "events" WHERE "events"."id" IN (14, 15, 16)
u.events.any? # => true
# (no query happening)
u.attendances.any? # => true
(1.1ms)  SELECT COUNT(*) FROM "attendances" WHERE "attendances"."user_id" = 1

Why did ActiveRecord do a count query when it should already have the attendances loaded?

So I changed the order of the includes:

u = User.includes([:events, :attendances]).find(1) # => #<User>
User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Attendance Load (0.6ms)  SELECT "attendances".* FROM "attendances" WHERE "attendances"."user_id" IN (1)
Event Load (0.5ms)  SELECT "events".* FROM "events" WHERE "events"."id" IN (14, 15, 16)
Attendance Load (0.4ms)  SELECT "attendances".* FROM "attendances" WHERE "attendances"."user_id" IN (1)
u.events.any? # => true
# (no query happening)
u.attendances.any? # => true
# (no query happening)

Now ActiveRecord didn't make any count queries, but instead it queried the attendances initially TWICE? And didn't even fall back to a cache for the same query?

What should you do?

Well, choose between pest or cholera:

Pest: If you include the :through association (attendances) before the more distant association (events), the closer association will NOT be loaded.

Cholera: But if you include the :through association after the more distant associaton, the closer association will be loaded TWICE.

You choose. (I prefer Cholera)

2 Responses
Add your response

Very helpful!! Thanks

over 1 year ago ·

Have you tried simply:

u = User.includes({ events: :attendances }).find(1)

?

over 1 year ago ·