Search code examples
ruby-on-railsrails-activerecordruby-on-rails-7

How to preload associations of STI models in Rails 7?


My Problem

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.

What I tried

  1. Of course
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

Solution

  • 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