【问题标题】:ActiveRecord nested associationsActiveRecord 嵌套关联
【发布时间】:2019-02-24 18:31:16
【问题描述】:

我有一些模型,例如 Service、Payment 和 Manager

class Service < ApplicationRecord
  validates :name, presence: true

  has_and_belongs_to_many :tickets
  has_many :payments
end

class Payment < ApplicationRecord
  belongs_to :manager
  belongs_to :service
  validates :value, :manager_id, :service_id, presence: true
end

class Manager < ApplicationRecord
  validates :name, presence: true
end

Service 可以有多个Payment,每个Payment 有一个Manager。

我想使用所有嵌套关联作为哈希来获取整个数据,我可以重新格式化(或映射)并发送给客户端。我被查询困住了:

Service.includes(payments: :manager).references(:payments, :managers)

因为它正在使用延迟加载,所以我需要执行以下操作:

services.first.payments.first.manager

获取数据,并且是非最优的。

是否可以获取所有嵌套关联的所有数据?

我做了这样的计算:

services = Service.includes(payments: :manager)
                   .references(:payments, :managers)

  result = []

  services.each do |service|
    service.payments.each do |payment|
      manager_name = payment[:manager][:name]
      value = payment[:value]

      service[manager_name] = value
    end

    result.push(service)
  end

得到一个错误NoMethodError (undefined method '[]' for nil:NilClass): 在manager_name = payment[:manager][:name] 线上。

【问题讨论】:

  • 我不明白问题出在哪里,您能否显示一些控制台输出以说明您所看到的内容?
  • 数据库上有多少服务?解决方案取决于数据的大小。
  • 事实上数据的大小并不重要,我想有一个独特的解决方案来一次获取所有数据。请参考已编辑的主题。
  • @Dmitry 不是service 类的Service 实例吗?但是您将其视为像service[manager_name] = value 这样的哈希。我有点困惑。

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


【解决方案1】:

你可以定义一个额外的 has_many through 关系,它应该给你你想要的。

has_many :managers, through: :payments

class Service < ApplicationRecord
  validates :name, presence: true

  has_and_belongs_to_many :tickets
  has_many :payments

  has_many :managers, through: :payments
end

class Payment < ApplicationRecord
  belongs_to :manager
  belongs_to :service
  validates :value, :manager_id, :service_id, presence: true
end

class Manager < ApplicationRecord
  validates :name, presence: true
end

【讨论】:

    【解决方案2】:

    我不明白您的需要 %100,但我想出了如下想法。请审查它。

    payments = Payment.joins(:service).select(:service_id, :manager_id, :value).
        group_by(&:service_id)
    
    managers = Manager.where('id IN(SELECT manager_id FROM payments)').
        select(:name,:id)
        group_by(&:id).transform_values{ |value| value.first }
    
    
    result = []
    ResultClass = Struct.new(:service, :manager, :payment_value)
    Service.all.each do |service|
      payment = payments[service.id]
      manager = managers[payment.manager_id]
      result << ResultClass.new(service, manager, payment.value)
    end
    

    使用这段代码,我们实际上是在尝试在底层实现 rails includes 机制。我们在这里实际做了什么?

    • 我们收到付款并将其转换为 id 为哈希键的哈希
    • 然后我们得到有付款的经理,我们还将这些记录转换为经理 ID 作为哈希键
    • 我们创建了ResultClass 和Struct 来存储对象的引用,例如service 和manager
    • 我们开始迭代。因为我们有payments 和members 作为哈希,所以查找速度很快,没有时间复杂度。总的来说,本次迭代的时间复杂度为 O(n)

    如果您有任何问题,请随时发表评论。

    【讨论】:

      【解决方案3】:

      经过多次尝试,我终于找到了一些我犯的拼写错误并归档了我想要使用此类代码的行为

      services = Service.eager_load(payments: :manager)
      
        services.reduce([]) do |acc, service|
          service_rec = {
            id: service[:id],
            name: service[:name],
            surgery: service[:surgery]
          }
      
          service.payments.each do |payment|
            manager_name = payment.manager[:name]
            value = payment[:value]
      
            service_rec[manager_name] = value
          end
      
          acc.push(service_rec)
        end
      

      以及它产生的 SQL 查询

      SQL (0.2ms)  SELECT "services"."id" AS t0_r0, "services"."code" AS t0_r1, "services"."name" AS t0_r2, "services"."surgery" AS t0_r3, "services"."enabled" AS t0_r4, "services"."created_at" AS t0_r5, "services"."updated_at" AS t0_r6, "payments"."id" AS t1_r0, "payments"."service_id" AS t1_r1, "payments"."manager_id" AS t1_r2, "payments"."value" AS t1_r3, "payments"."created_at" AS t1_r4, "payments"."updated_at" AS t1_r5, "managers"."id" AS t2_r0, "managers"."name" AS t2_r1, "managers"."enabled" AS t2_r2, "managers"."created_at" AS t2_r3, "managers"."updated_at" AS t2_r4 FROM "services" LEFT OUTER JOIN "payments" ON "payments"."service_id" = "services"."id" LEFT OUTER JOIN "managers" ON "managers"."id" = "payments"."manager_id"
      

      有趣的是,使用includes 而不是eager_load 会产生三个查询

      Service Load (0.1ms)  SELECT "services".* FROM "services"
        ↳ app/services/payments_service.rb:6
        Payment Load (0.1ms)  SELECT "payments".* FROM "payments" WHERE "payments"."service_id" IN (?, ?, ?, ?, ?, ?, ?, ?)  [["service_id", 1], ["service_id", 2], ["service_id", 3], ["service_id", 4], ["service_id", 5], ["service_id", 6], ["service_id", 7], ["service_id", 8]]
        ↳ app/services/payments_service.rb:6
        Manager Load (0.1ms)  SELECT "managers".* FROM "managers" WHERE "managers"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
      

      我们也可以使用Service.includes(payments: :manager).references(:payments, :managers) 并获得与eager_load 相同的查询,但输入时间更长))

      感谢大家的参与!有人对eager_load 或代码优化建议有其他意见吗?

      【讨论】:

      • 你测试了我的方法吗?我的方法的输出是什么?如果你分享它,你介意我吗?
      • @zeitnot 抱歉,我没有测试你的代码,这对我来说似乎太复杂了(可能是因为我是 activerecord 新手))无论如何,如果我们可以使用单一和简单的方式获取数据,为什么我们需要 3 个查询查询?
      • 其实,我并没有真正了解您的需求。所以我认为你对 rails 的includes 有一些问题,我为此做了一个解决方法。您会想在速度方面比较我的解决方案和您的解决方案。我不确定哪个是最好的,因为你有数据。
      猜你喜欢
      • 2011-10-06
      • 1970-01-01
      • 2016-07-26
      • 1970-01-01
      • 2011-10-25
      • 2012-07-26
      • 1970-01-01
      • 1970-01-01
      • 2021-12-19
      相关资源
      最近更新 更多