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 (?, ?, ?)
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 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?
Try this:
> person_id = 1
> person = Person.includes(:favorite_car, :business_car, :home_car).where("people.id" = ?", person_id).references(:favorites_car, :business_car, :home_car)
> person[0].favorite_car
app/models/person.rb
class Person < ActiveRecord::Base
# columns: id, name, favorite_car_id, business_car_id, home_car_id
belongs_to :favorite_car, class_name: 'Car'
belongs_to :business_car, class_name: 'Car'
belongs_to :home_car, class_name: 'Car'
end
app/models/car.rb
class Car < ActiveRecord::Base
# columns: id, name
has_many :people
end
Proof that this works:
> Person.all
Person Load (0.3ms) SELECT "people".* FROM "people"
=> #<ActiveRecord::Relation [#<Person id: 1, favorite_car_id: 1, business_car_id: 2, home_car_id: 3, name: "Frankie", created_at: "2014-02-18 21:51:58", updated_at: "2014-02-18 21:53:34">]>
> Car.all
Car Load (0.3ms) SELECT "cars".* FROM "cars"
=> #<ActiveRecord::Relation [#<Car id: 1, name: "Mazda", created_at: "2014-02-18 21:52:16", updated_at: "2014-02-18 21:52:16">, #<Car id: 2, name: "Honda", created_at: "2014-02-18 21:52:20", updated_at: "2014-02-18 21:52:20">, #<Car id: 3, name: "BMW", created_at: "2014-02-18 21:52:24", updated_at: "2014-02-18 21:52:24">]>
> Person.includes(:favorite_car, :business_car, :home_car).where("people.id = ?", 1).references(:favorite_car, :business_car, :home_car)
SQL (0.4ms) SELECT "people"."id" AS t0_r0, "people"."favorite_car_id" AS t0_r1, "people"."business_car_id" AS t0_r2, "people"."home_car_id" AS t0_r3, "people"."name" AS t0_r4, "people"."created_at" AS t0_r5, "people"."updated_at" AS t0_r6, "cars"."id" AS t1_r0, "cars"."name" AS t1_r1, "cars"."created_at" AS t1_r2, "cars"."updated_at" AS t1_r3, "business_cars_people"."id" AS t2_r0, "business_cars_people"."name" AS t2_r1, "business_cars_people"."created_at" AS t2_r2, "business_cars_people"."updated_at" AS t2_r3, "home_cars_people"."id" AS t3_r0, "home_cars_people"."name" AS t3_r1, "home_cars_people"."created_at" AS t3_r2, "home_cars_people"."updated_at" AS t3_r3 FROM "people" LEFT OUTER JOIN "cars" ON "cars"."id" = "people"."favorite_car_id" LEFT OUTER JOIN "cars" "business_cars_people" ON "business_cars_people"."id" = "people"."business_car_id" LEFT OUTER JOIN "cars" "home_cars_people" ON "home_cars_people"."id" = "people"."home_car_id" WHERE (people.id = 1)
=> #<ActiveRecord::Relation [#<Person id: 1, favorite_car_id: 1, business_car_id: 2, home_car_id: 3, name: "Frankie", created_at: "2014-02-18 21:51:58", updated_at: "2014-02-18 21:53:34">]>
Important Note: Rails will automagically use people
as the plural of person
. So when you create a Person
model, it will create a people
database table.
There is a great post about manual Eager load.
http://mrbrdo.wordpress.com/2013/09/25/manually-preloading-associations-in-rails-using-custom-scopessql/
I guess this is what you have been looking for:
owners = People.all
association_name = :photos
owners.each do |owner|
record = whatever_you_want
association = owner.association(association_name)
association.target = record
association.set_inverse_instance(record)
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With