The problem
I have the following ActiveRecord model:
class Person
belongs_to :favourite_car, class_name: 'Car'
belongs_to :business_car, class_name: 'Car'
belongs_to :home_car, class_name: 'Car'
end
When I want to access all three of these associations, it generates three select queries:
SELECT * FROM cars WHERE cars.id = ?
This is essentially the N+1 problem.
Ideally, I'd like it to generate just one query of the form
SELECT * FROM cars WHERE cars.id IN (?, ?, ?)
Possible solution
I could move this to a has_many :through => :join_table
association with a column in the join table to indicate what the association type is, then use includes([:join_table, :cars])
to eager load the associations. However, in this case it only reduces 3 queries to 2, and introduces an extra table.
Another possible solution
Another possible solution would be to manually load the associations like so:
module EagerLoader
def eager_load(*associations)
reflections = associations.map { |association| self.class.reflections[association.to_sym] }
raise 'Not all are valid associations' if reflections.any?(&:nil?)
reflections.group_by { |association| association.klass }.each do |klass, reflections|
load_associations(klass, reflections)
end
self
end
private
def load_associations(klass, reflections)
primary_key = klass.primary_key
ids = reflections.map { |reflection| public_send(reflection.foreign_key) }
records = klass.where(id: ids)
reflections.each_with_index do |reflection, i|
record = records.find do |record|
record.public_send(primary_key) == ids[i]
end
public_send("#{reflection.name}=", record)
end
end
end
I have tested it, and it works.
class Person
include EagerLoader
end
Person.find(2).eager_load(:favorite_car, :business_car, :home_car)
However, this still does not help you when you want to do things like
Person.includes(:favourite_car, :business_car, :home_car)
For example, on the person index page. This reduces the number of queries from 3N+1 to 4, but only 2 are actually needed.
Are there any better solutions to this problem?