Imagine I've got those models:
class Absence << ApplicationRecord
belongs_to :user
end
class Vacation << Absence
belongs_to :vacation_contingent
end
class Illness << Absence; end
Now I want to retrieve all absences with
absences = Absence.where(user: xxx)
and iterate over the vacation contingents
vacations = absences.select { |absence| absence.is_a?(Vacation)
vacations.each { |vacation| puts vacation.vacation_contingent.xxx }
Now I've got 1 database query for those absences and 1 for each vacation_contingent -> bad
PS: I use Absence.where
instead of Vacation.where
because I want to do other things with those absences
.
Absence.where(user: xxx).includes(:vacation_contingent)
# -> ActiveRecord::AssociationNotFoundError Exception: Association named 'vacation_contingent' was not found`
vacations = Vactions.where(user: xxx).includes(:vacation_contingent)
other_absences = Absence.where(user: xxx).where.not(type: 'Vacation')
But this one is ugly and I've got 1 database query more than I want to because I'm fetchting the absences 2 times.
3.
absences = Absence.where(user: xxx)
vacations = absences.select { |absence| absence.is_a?(Vacation)
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(vacations, :vacation_contingent)
# -> ArgumentError Exception: missing keywords: :records, :associations
# -> (The initializer changed)
absences = Absence.where(user: xxx)
vacations = absences.select { |absence| absence.is_a?(Vacation)
preloader = ActiveRecord::Associations::Preloader.new(records: vacations, associations: %i[vacation_contingent])
# -> This line does nothing on it's own
preloader.call
# -> This just executes SELECT "vacation_contingents".* FROM "vacation_contingents" vacation.size times
preloader.preload
# -> Does the same as .call
# -> And this doesn't even preload anything. When executing
vacations.first.vacation_contingent
# -> then the database is being asked again
I found a solution. The ActiveRecord::Associations::Preloader.new(records: vacations, associations: %i[vacation_contingent])
part was the trick. My mistake was that I simply called preloader.call
inside the rails console and this tried to output something. If I call preloader.call; nil
, it just works fine and preloads stuff.
TL;DR
absences = Absence.where(user: xxx)
vacations = absences.select { |absence| absence.is_a?(Vacation)
ActiveRecord::Associations::Preloader.new(records: vacations, associations: %i[vacation_contingent]).call # add a `; nil` if executed inside the rails console
vacations.each { |vacation| puts vacation.vacation_contingent.xxx }
# => no n+1 queries