Высокая загрузка в зависимости от типа ассоциации в Ruby on Rails

У меня есть полиморфная ассоциация (belongs_to :resource, polymorphic: true), где resource может быть множеством различных моделей. Чтобы упростить вопрос, предположим, что это может быть либо Order, либо Customer.

Если это Order, я хотел бы предварительно загрузить заказ и предварительно загрузить Address. Если это клиент, я хотел бы предварительно загрузить Customer и предварительно загрузить Location.

Код с использованием этих ассоциаций делает что-то вроде:

<%- @issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.resource.name %> <%= issue.resource.location.name %>
<%- when Order -%>
<%= issue.resource.number %> <%= issue.resource.address.details %>
<%- end -%>

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

@issues.preload(:resource)

Однако я все еще вижу проблемы n-plus-one для загрузки условных ассоциаций:

SELECT "addresses".* WHERE "addresses"."order_id" = ...
SELECT "locations".* WHERE "locations"."customer_id" = ...
...

Какой хороший способ исправить это? Возможно ли предварительно предустановить связь?

Ответ 1

Вы можете сделать это с помощью класса ActiveRecord::Associations::Preloader. Вот код:

@issues = Issue.all # Or whatever query
ActiveRecord::Associations::Preloader.new.preload(@issues.select { |i| i.resource_type == "Order" }, { resource: :address })
ActiveRecord::Associations::Preloader.new.preload(@issues.select { |i| i.resource_type == "Customer" }, { resource: :location })

При фильтрации коллекции вы можете использовать другой подход. Например, в моем проекте я использую group_by

groups = sale_items.group_by(&:item_type)
groups.each do |type, items|
  conditions = case type
  when "Product" then :item
  when "Service" then { item: { service: [:group] } }
end

ActiveRecord::Associations::Preloader.new.preload(items, conditions)

Вы можете легко обернуть этот код в каком-то вспомогательном классе и использовать его в разных частях вашего приложения.

Ответ 2

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

class Issue
  belongs_to :order
  belongs_to :customer

  # You should validate that one and only one of order and customer is present.

  def resource
    order || customer
  end
end

Issue.preload(order: :address, customer: :location)

Я на самом деле написал камень, который завершает этот шаблон, чтобы синтаксис становился

class Issue
  has_owner :order, :customer, as: :resource
end

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

Ответ 3

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

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

ATTRIBS = {
  'Order' => [:address],
  'Customer' => [:location]
}.freeze

AVAILABLE_TYPES = %w(Order Customer).freeze

В приведенном выше списке перечислены ассоциации, которые с нетерпением загружают доступные типы реализации.

Теперь в нашем коде мы просто перейдем через AVAILABLE_TYPES, а затем загрузим необходимые ассоциации.

issues = []

AVAILABLE_TYPES.each do |type|
  issues += @issues.where(resource_type: type).includes(resource: ATTRIBS[type])
end

Благодаря этому у нас есть способ для предварительной загрузки ассоциаций, основанных на типе. Если у вас есть другой тип, просто добавьте его в AVAILABLE_TYPES и атрибуты ATTRIBS, и все будет готово.

Ответ 4

Вам необходимо определить ассоциации в таких моделях:

class Issue < ActiveRecord::Base
  belongs_to :resource, polymorphic: true

  belongs_to :order, -> { includes(:issues).where(issues: { resource_type: 'Order' }) }, foreign_key: :resource_id
  belongs_to :customer, -> { includes(:issues).where(issues: { resource_type: 'Customer' }) }, foreign_key: :resource_id
end

class Order < ActiveRecord::Base
  belongs_to :address
  has_many :issues, as: :resource
end

class Customer < ActiveRecord::Base
  belongs_to :location
  has_many :issues, as: :resource
end

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

Issue.includes(order: :address, customer: :location).all

В представлениях вы должны использовать явное имя отношения:

<%- @issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.customer.name %> <%= issue.customer.location.name %>
<%- when Order -%>
<%= issue.order.number %> <%= issue.order.address.details %>
<%- end -%>

Это все, больше запросов n-plus-one.

Ответ 5

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

i имеет модель address, которая полиморфна до user и property. Поэтому я просто проверяю addressable_type вручную, а затем вызываю соответствующий запрос, как показано ниже: -

после получения либо user, либо property, я получаю address с желаемой загрузкой необходимых моделей

##@record can be user or property instance
if @record.class.to_s == "Property"
     Address.includes(:addressable=>[:dealers,:property_groups,:details]).where(:addressable_type=>"Property").joins(:property).where(properties:{:status=>"active"})
else if @record.class.to_s == "User"
     Address.includes(:addressable=>[:pictures,:friends,:ratings,:interests]).where(:addressable_type=>"User").joins(:user).where(users:{is_guest:=>true})
end

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

Надеюсь, что это поможет.

Ответ 6

Если вы создаете экземпляр связанного объекта в качестве объекта, о котором идет речь, например. назовите это переменная @object или некоторые такие. Затем рендер должен обработать определение правильного представления через класс объекта. Это соглашение Rails, т.е. магия рельсов.

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

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