【问题标题】:Eager Load Depending on Type of Association in Ruby on Rails急切负载取决于 Ruby on Rails 中的关联类型
【发布时间】:2017-03-13 21:00:28
【问题描述】:

我有一个多态关联 (belongs_to :resource, polymorphic: true),其中 resource 可以是各种不同的模型。为了简化问题,假设它可以是OrderCustomer

如果是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 加一问题:

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

有什么好的方法可以解决这个问题?是否可以手动预加载关联?

【问题讨论】:

  • 由于您面临的问题等原因,我不再使用多态关联。 (它们还使数据完整性变得更加困难)。
  • Rails 非常智能。你试过@issues.preloads(resouce: [:location, :address])吗?
  • @MarcRohloff 它引发了一个异常ActiveRecord::AssociationNotFoundError。看起来这行不通(尽管这样的语法是理想的)。
  • @WizardofOgz 我想你获得并失去了一些完整性。使用单独的列,您可能会遇到没有设置任何列的情况 - 或者设置了所有列 - 或者设置了一些列。也就是说 - 它确实使设置外键约束更容易......
  • @stussa 您可以(并且应该)添加数据库约束以仅允许设置一列。这是我的开发团队使用的约束示例。它确保设置了一组 FK 中的一个 CONSTRAINT owner_mutex_required CHECK (1 = (license_id IS NOT NULL)::integer + (contract_id IS NOT NULL)::integer + (policy_id IS NOT NULL)::integer)

标签: ruby-on-rails rails-activerecord polymorphic-associations


【解决方案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】:

    这现在在 Rails 中工作 v6.0.0.rc1: https://github.com/rails/rails/pull/32655

    你可以.includes(resource: [:address, :location])

    【讨论】:

      【解决方案3】:

      您可以将多态关联分解为单独的关联。我遵循了这一点,并对它如何简化我的应用程序感到非常满意。

      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
      

      并适当地设置关联和验证。不幸的是,该实施不是公开的或公开的。不过,做自己并不难。

      【讨论】:

      • 感谢您的回答。不幸的是,我正在寻找一种适用于多态关联的解决方案,并且无法将我的关联更改为非多态(在传统意义上)。
      【解决方案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 加一查询。

      【讨论】:

        【解决方案5】:

        我想分享一个我用于条件预加载的查询,但不确定这是否对您有帮助,我不确定,但值得一试。

        我有一个address 模型,它与userproperty多态。 所以我只是手动检查addressable_type,然后调用适当的查询,如下所示:-

        在获得userproperty 后,我获得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
        

        上面的查询是实际查询的一个小sn-p,但是您可以了解如何使用joins进行预加载,因为它是一个多态表

        希望对你有帮助。

        【讨论】:

          【解决方案6】:

          如果您将关联对象实例化为相关对象,例如称它为变量@object 或类似的。 Then the render should handle the determination of the correct view via the object's class. 这是 Rails 约定,即 Rails 的魔力。

          我个人讨厌它,因为如果没有 byebug 或 pry 之类的东西很难调试错误的当前范围,但我可以证明它确实有效,因为我们在我的雇主这里使用它来解决类似的问题。

          我认为通过这种方法和rails缓存可以更好地解决速度问题,而不是通过预加载更快。

          【讨论】:

            【解决方案7】:

            当我遇到这个问题时,我为自己想出了一个可行的解决方案。我遵循的是遍历每种类型的实现并将其连接到一个数组中。

            首先,我们将首先记下将为特定类型加载哪些属性。

            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,即可完成。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2015-08-05
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多