预加载 has_many 与动态条件的关联

Posted

技术标签:

【中文标题】预加载 has_many 与动态条件的关联【英文标题】:Preload has_many associations with dynamic conditions 【发布时间】:2014-07-08 16:56:17 【问题描述】:

我有一个 Place 模型和一个 Event 模型。地点可以举办在特定日期发生的活动。

如何设置我的关联和查找器以加载所有地点,包括(急切加载)他们在特定日期的事件而不会出现 N+1 查询问题?

我尝试过的:

class Place
    has_many :events
end

Place.all.preload(:events).where("events.start_date > '#time_in_the_future'")
#ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "events".

Place.all.includes(:events).where("events.start_date > '#time_in_the_future'").references(:event)
# only loads places that have an event at the specific date and not all places including their events (if there are any events).

我成功地想出了一个可以做我想做但不是动态的关联(不接受参数)

class Place
    has_many :events, -> where("events.start_date > '#Time.now'")
end

Place.all.preload(:events)
# perfect: executes two queries: One to get all 'places' and one to get all 'events' that belong to the places and merges the 'events' into the 'place' objects. 
# But I can't pass time as a parameter, so time is always Time.now (as specified in the has_many association). 
# Place.all.preload(:events).where(xyz) gives wrong results like the examples above.

对我来说,问题是我找不到在动态条件下预加载/急切加载的方法。因为预加载和包含期望关联名称作为参数并且不能使用参数进行细化。至少我发现没有办法做到这一点。

【问题讨论】:

这是一篇很好的文章,解释了 Rails 中的预加载:blog.arkency.com/2013/12/rails4-preloading 谢谢,但已经读了几遍这篇文章,但是在预加载时找不到动态条件的信息。 也找不到将参数传递给 ActiveRecord::Associations::Preloader 的方法。覆盖或子类化似乎是一种错误的方法。 【参考方案1】:

这似乎是唯一可行的解​​决方案:

# 1st query: load places
places = Place.all.to_a

# 2nd query: load events for given places, matching the date condition
events = Event.where(place: places.map(&:id)).where("start_date > '#time_in_the_future'")
events_by_place_id = events.group_by(&:place_id)

# 3: manually set the association
places.each do |place|
  events = events_by_place_id[place.id] || []

  association = place.association(:events)
  association.loaded!
  association.target.concat(events)
  events.each  |event| association.set_inverse_instance(event) 
end

这有点 hacky,但它很容易适应您可能希望使用单独的查询加载关联然后将其附加到现有对象的任何情况。

所有功劳归https://mrbrdo.wordpress.com/2013/09/25/manually-preloading-associations-in-rails-using-custom-scopessql/

【讨论】:

简直太棒了……终于找到了可行的方法。有一件事 - 第一步中的to_a 会阻止您进行额外的查询链接等,但您实际上不需要将位置转换为数组。 第一个查询只是一个示例,您不需要显式转换为数组(无论如何,map 会执行)。如果需要,您还可以添加其他过滤器。你明白了。【参考方案2】:

解决动态日期问题,你有没有考虑过:

class Event < ActiveRecord::Base

  belongs_to :place

  scope :on_date, lambda |the_date| where(start_date: the_date) 
  scope :on_or_after, lambda |the_date| where('start_date >= ?', the_date) 
end

然后你可以这样做:

@place = Place.find(params[:id]) # let's say...
@place.events.on_date(params[:chosen_date])

您也可以合并其他人提到的急切加载内容。

【讨论】:

【参考方案3】:

据我了解,您希望获取至少有一个事件满足某些条件的所有地点,但应获取包含所有事件列表的地点,即使是那些不满足条件的地点。您无法通过一个简单的查询来解决这个问题,但如果您将使用 suquery,那么问题就会完成。这是解决方案:

Place.includes(:events).where(id: Place.joins(:events).where("events.start_date > '#time_in_the_future'")).references(:events)

将构造一个复杂的查询,但它做对了。

【讨论】:

【参考方案4】:

我提到在某些情况下includes 没有正确选择急切加载方法。有一个解释这个方法是如何工作的 http://blog.arkency.com/2013/12/rails4-preloading/ 。您可以直接调用eager_load(:events) 我认为它会加载您的 AR 对象而不会出现 n+1 问题。

【讨论】:

以上是关于预加载 has_many 与动态条件的关联的主要内容,如果未能解决你的问题,请参考以下文章

Gorm 预加载及实现联表条件查询仿WhereHas

在linux中预加载动态加载的库

Laravel 用户模型条件预加载(带)

Entity Framework 4.1 - 动态预加载

三十PHP框架Laravel学习笔记——模型的预加载

图片预加载与懒加载