想象一下我有这些模型:
class Absence << ApplicationRecord
belongs_to :user
end
class Vacation << Absence
belongs_to :vacation_contingent
end
class Illness << Absence; end
现在我想用
检索所有缺席情况absences = Absence.where(user: xxx)
并迭代假期安排
vacations = absences.select { |absence| absence.is_a?(Vacation)
vacations.each { |vacation| puts vacation.vacation_contingent.xxx }
现在我有 1 个针对这些缺勤的数据库查询,以及 1 个针对每个 Vacation_contingent -> bad
PS:我使用
Absence.where
而不是 Vacation.where
,因为我想用这些 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')
但是这个很丑,而且我的数据库查询比我想要的多 1 个,因为我要获取缺勤 2 次。
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
在我看来,解决方案 2 是 ActiveRecord 能做到的最好的解决方案。
如果你只想要一个请求,你可以使用原始 SQL 来完成;类似的东西:
Absence.connection.select_all(%{SELECT *
FROM absences
LEFT OUTER JOIN vacation_contingents ON absences.vacation_contingents_id = vacation_contingents.id
WHERE absences.user_id = ?", user_xxx.id})
它将返回 ActiveRecord::Result,每个
Absence
各一行,以及 Absence
和 VacationContingent
的两列
我找到了解决办法。
ActiveRecord::Associations::Preloader.new(records: vacations, associations: %i[vacation_contingent])
部分就是窍门。我的错误是我只是在 Rails 控制台内调用了 preloader.call
并尝试输出一些内容。如果我调用 preloader.call; nil
,它就会正常工作并预加载内容。
TL;博士
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