Высокая загрузка в зависимости от типа ассоциации в 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, но я могу подтвердить, что она работает, поскольку мы используем ее здесь у моего работодателя для решения подобной проблемы.
Вместо более быстрой предварительной загрузки, я думаю, что проблема скорости лучше решается с помощью этого метода и кэширования рельсов.
Ответ 7
Теперь это работает в Rails v6.0.0.rc1
: https://github.com/rails/rails/pull/32655
Вы можете сделать .includes(resource: [:address, :location])