Preload has_many ассоциации с динамическими условиями

У меня есть модель 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.

Проблема для меня в том, что я не могу найти способ загружать/загружать динамические условия. Поскольку preload и include ожидают имя ассоциации как параметр и не могут быть уточнены с параметрами. По крайней мере, я не нашел возможности сделать это.

Ответ 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

Немного взломанный, но довольно легко адаптироваться к любой ситуации, когда вам может понадобиться загрузить ассоциацию с использованием отдельного запроса, а затем прикрепить его к существующему объекту.

Все кредиты относятся к https://mrbrdo.wordpress.com/2013/09/25/manually-preloading-associations-in-rails-using-custom-scopessql/

Ответ 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 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.