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)
Written by Robert Wünsch
Related protips
2 Responses
Very helpful!! Thanks
Have you tried simply:
u = User.includes({ events: :attendances }).find(1)
?