Rails polymorphic with includes на основе типа класса

Скажем, мы имеем эти модели

class Message
  belongs_to :messageable, polymorphic: true
end

class Ticket
  has_many :messages, as: :messageable
  has_many :comments
end

class User
  has_many :messages, as: :messageable
  has_many :ratings
end

class Rating
  belongs_to :user
end

class Comment
  belongs_to :ticket
end

Теперь я хочу загрузить все сообщения (которые связаны с tickets или users) и загружать загрузку в зависимости от типа класса, либо comments для tickets и ratings для users

Конечно, Message.includes(:messageable).order("created_at desc") будет включать только связанный с ним объект, но вопрос заключается в том, как включить различные типы ассоциаций, которые производятся от каждого типа модели (т.е. в этом примере, как загружать нагрузку comments for tickets и ratings for users)?

Это простой пример, но что еще более сложные случаи, когда я хотел бы добавить что-то еще для user, другую ассоциацию, и что, если эта ассоциация нуждается в большей части?

Ответ 1

Единственный способ, которым я могу это сделать, - дублировать ассоциации для каждой модели с общим именем:

class Ticket
  has_many :messages, as: :messageable
  has_many :comments
  has_many :messageable_includes, class_name: "Comment"
end

class User
  has_many :messages, as: :messageable
  has_many :ratings
  has_many :messageable_includes, class_name: "Rating"
end

Message.includes(:messageable => :messageable_includes) ...

Я не уверен, что буду использовать эту стратегию в качестве широко распространенного решения, но если это сложно по мере вашего дела, это может сработать для вас.

Ответ 2

В моем собственном проекте я использовал следующие вспомогательные методы:

def polymorphic_association_includes(association, includes_association_name, includes_by_type)
  includes_by_type.each_pair do |includes_association_type, includes|
    polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
  end
end

def polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
  id_attr = "#{includes_association_name}_id"
  type_attr = "#{includes_association_name}_type"

  items = association.select {|item| item[type_attr] == includes_association_type.to_s }
  item_ids = items.map {|item| item[id_attr] }
  items_with_includes = includes_association_type.where(id: item_ids).includes(includes).index_by(&:id)

  items.each do |parent|
    parent.send("#{includes_association_name}=", items_with_includes[parent[id_attr]])
  end
end

Это позволит вам сказать:

messages = Message.all
polymorhpic_association_includes messages, :messageable, {
  Ticket => :comments,
  User => :ratings
}

Не особенно плавный интерфейс, но он работает в целом.

Ответ 3

Поместите объекты в область по умолчанию для каждой модели:

class Ticket
  has_many :messages, as: :messageable
  has_many :comments
  default_scope -> { includes(:comments).order('id DESC') }
end

class User
  has_many :messages, as: :messageable
  has_many :ratings
  default_scope -> { includes(:ratings).order('id DESC') }
end

Тогда всякий раз, когда вы вызываете Message.all, каждая полиморфная ассоциация будет включать в себя ее собственные ресурсы.

Также, если вам нужно вызвать класс без области, просто используйте unscoped или создайте другую область:

class Ticket
  has_many :messages, as: :messageable
  has_many :comments
  has_many :watchers
  default_scope -> { includes(:comments).order('id DESC') }
  scope :watched -> {includes(:watchers)}
end

Ticket.unscoped.all # without comments or watchers (or order)
Ticket.watched.all  # includes watchers only