Rails 上 Ruby 中取决于关联类型的预加载
Eager Load Depending on Type of Association in 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" = ...
...
解决这个问题的好方法是什么?是否可以手动预加载关联?
您可以将多态关联分解为单独的关联。我已经关注了它,并且对它如何简化我的应用程序感到非常高兴。
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)
我实际上写了一个 gem 来包装这个模式,这样语法就变成了
class Issue
has_owner :order, :customer, as: :resource
end
并适当地设置关联和验证。不幸的是,该实现未开放或 public。不过自己做起来也不难。
您可以在 ActiveRecord::Associations::Preloader
class 的帮助下做到这一点。这是代码:
@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)
您可以轻松地将此代码包装在一些帮助程序中 class 并在您应用的不同部分使用它。
当我陷入这个问题时,我为自己想出了一个可行的解决方案。我遵循的是遍历每种类型的实现并将其连接到一个数组中。
首先,我们将首先记下将为特定类型加载的属性。
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
,即可完成。
您需要像这样在模型中定义关联:
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加一查询。
我想分享我用于条件预加载的一个查询,但不确定这是否对您有帮助,我不确定但值得一试。
我有一个 address
模型,它是 多态 到 user
和 property
。
所以我只是手动检查 addressable_type
然后调用适当的 query 如下所示:-
在获得 user
或 property
之后,我得到 address
到 eager loading 所需的模型
##@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
上面的查询是实际查询的一小段,但是您可以了解如何使用 joins 将其用于预加载,因为它是多态的 table.
希望对您有所帮助。
如果您将关联对象实例化为相关对象,例如称它为变量 @object 或类似的。 Then the render should handle the determination of the correct view via the object's class. 这是一个 Rails 惯例,即 rails' 魔法。
我个人讨厌它,因为如果没有 byebug 或 pry 之类的东西很难调试当前范围的错误,但我可以证明它确实有效,因为我们在我的雇主这里使用它来解决类似的问题。
我认为通过这种方法和 rails 缓存可以更好地解决速度问题,而不是通过预加载更快。
现在在 Rails v6.0.0.rc1
中工作:https://github.com/rails/rails/pull/32655
你可以做到.includes(resource: [:address, :location])
我有一个多态关联 (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" = ...
...
解决这个问题的好方法是什么?是否可以手动预加载关联?
您可以将多态关联分解为单独的关联。我已经关注了它,并且对它如何简化我的应用程序感到非常高兴。
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)
我实际上写了一个 gem 来包装这个模式,这样语法就变成了
class Issue
has_owner :order, :customer, as: :resource
end
并适当地设置关联和验证。不幸的是,该实现未开放或 public。不过自己做起来也不难。
您可以在 ActiveRecord::Associations::Preloader
class 的帮助下做到这一点。这是代码:
@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)
您可以轻松地将此代码包装在一些帮助程序中 class 并在您应用的不同部分使用它。
当我陷入这个问题时,我为自己想出了一个可行的解决方案。我遵循的是遍历每种类型的实现并将其连接到一个数组中。
首先,我们将首先记下将为特定类型加载的属性。
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
,即可完成。
您需要像这样在模型中定义关联:
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加一查询。
我想分享我用于条件预加载的一个查询,但不确定这是否对您有帮助,我不确定但值得一试。
我有一个 address
模型,它是 多态 到 user
和 property
。
所以我只是手动检查 addressable_type
然后调用适当的 query 如下所示:-
在获得 user
或 property
之后,我得到 address
到 eager loading 所需的模型
##@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
上面的查询是实际查询的一小段,但是您可以了解如何使用 joins 将其用于预加载,因为它是多态的 table.
希望对您有所帮助。
如果您将关联对象实例化为相关对象,例如称它为变量 @object 或类似的。 Then the render should handle the determination of the correct view via the object's class. 这是一个 Rails 惯例,即 rails' 魔法。
我个人讨厌它,因为如果没有 byebug 或 pry 之类的东西很难调试当前范围的错误,但我可以证明它确实有效,因为我们在我的雇主这里使用它来解决类似的问题。
我认为通过这种方法和 rails 缓存可以更好地解决速度问题,而不是通过预加载更快。
现在在 Rails v6.0.0.rc1
中工作:https://github.com/rails/rails/pull/32655
你可以做到.includes(resource: [:address, :location])