【问题标题】:ActiveRecord eager load multiple belongs_to associationsActiveRecord 急切加载多个 belongs_to 关联
【发布时间】:2014-02-18 20:37:07
【问题描述】:

问题

我有以下 ActiveRecord 模型:

class Person
  belongs_to :favourite_car, class_name: 'Car'
  belongs_to :business_car, class_name: 'Car'
  belongs_to :home_car, class_name: 'Car'
end

当我想访问所有这三个关联时,它会生成三个选择查询:

SELECT * FROM cars WHERE cars.id = ?

这本质上是 N+1 问题

理想情况下,我希望它只生成一个表单查询

SELECT * FROM cars WHERE cars.id IN (?, ?, ?)

可能的解决方案

我可以将其移至 has_many :through => :join_table 关联,并在连接表中使用一列指示关联类型,然后使用 includes([:join_table, :cars]) 急切加载关联。但是,在这种情况下,它仅将 3 个查询减少到 2 个,并引入了一个额外的表。

另一种可能的解决方案

另一种可能的解决方案是手动加载关联,如下所示:

module EagerLoader
  def eager_load(*associations)
    reflections = associations.map { |association| self.class.reflections[association.to_sym] }
    raise 'Not all are valid associations' if reflections.any?(&:nil?)

    reflections.group_by { |association| association.klass }.each do |klass, reflections|
      load_associations(klass, reflections)
    end

    self

  end

private

  def load_associations(klass, reflections)

    primary_key = klass.primary_key

    ids = reflections.map { |reflection| public_send(reflection.foreign_key) }

    records = klass.where(id: ids)


    reflections.each_with_index do |reflection, i|
      record = records.find do |record|
        record.public_send(primary_key) == ids[i]
      end

      public_send("#{reflection.name}=", record)
    end

  end


end

我已经测试过了,它可以工作。

class Person
  include EagerLoader
end

Person.find(2).eager_load(:favorite_car, :business_car, :home_car)

但是,当您想做类似的事情时,这仍然对您没有帮助

Person.includes(:favourite_car, :business_car, :home_car)

例如,在人员索引页面上。这将查询的数量从 3N+1 减少到 4,但实际上只需要 2 个。

有没有更好的办法解决这个问题?

【问题讨论】:

  • 最喜欢的汽车与商务车有何不同(即在模型意义上)?当您访问 Person 的汽车数据时,您通常需要所有三个位(最喜欢的、商务和家用汽车)吗?
  • favourite_car、business_car 和 home_car 都是 Car 的实例,所以模型没有区别。几乎所有时间,我都需要访问所有三个位或根本不访问。
  • 抱歉...没问得那么好。最喜欢的汽车与家用汽车的区别是什么(是否有区分这些模型的“布尔”或“类型”列)?换句话说,你怎么知道汽车是最喜欢的、企业还是家? (我真的在问一个理由......不要烦人:))。
  • 有没有理由一个人没有拥有_one而不是belongs_to?似乎这将是一个更容易设置。你的表结构是什么样的?
  • @craig.kaminsky 根据 belongs_to 关联的 activerecord 约定,people 表中有 favourite_car_id、business_car_id 和 home_car_id 外键。

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


【解决方案1】:

试试这个:

> person_id = 1
> person = Person.includes(:favorite_car, :business_car, :home_car).where("people.id" = ?", person_id).references(:favorites_car, :business_car, :home_car)
> person[0].favorite_car

app/models/person.rb

class Person < ActiveRecord::Base
  # columns: id, name, favorite_car_id, business_car_id, home_car_id
  belongs_to :favorite_car, class_name: 'Car'
  belongs_to :business_car, class_name: 'Car'
  belongs_to :home_car, class_name: 'Car'
end

app/models/car.rb

class Car < ActiveRecord::Base
  # columns: id, name
  has_many :people
end

证明这是有效的:

> Person.all
  Person Load (0.3ms)  SELECT "people".* FROM "people"
=> #<ActiveRecord::Relation [#<Person id: 1, favorite_car_id: 1, business_car_id: 2, home_car_id: 3, name: "Frankie", created_at: "2014-02-18 21:51:58", updated_at: "2014-02-18 21:53:34">]>

> Car.all
  Car Load (0.3ms)  SELECT "cars".* FROM "cars"
=> #<ActiveRecord::Relation [#<Car id: 1, name: "Mazda", created_at: "2014-02-18 21:52:16", updated_at: "2014-02-18 21:52:16">, #<Car id: 2, name: "Honda", created_at: "2014-02-18 21:52:20", updated_at: "2014-02-18 21:52:20">, #<Car id: 3, name: "BMW", created_at: "2014-02-18 21:52:24", updated_at: "2014-02-18 21:52:24">]>

> Person.includes(:favorite_car, :business_car, :home_car).where("people.id = ?", 1).references(:favorite_car, :business_car, :home_car)
  SQL (0.4ms)  SELECT "people"."id" AS t0_r0, "people"."favorite_car_id" AS t0_r1, "people"."business_car_id" AS t0_r2, "people"."home_car_id" AS t0_r3, "people"."name" AS t0_r4, "people"."created_at" AS t0_r5, "people"."updated_at" AS t0_r6, "cars"."id" AS t1_r0, "cars"."name" AS t1_r1, "cars"."created_at" AS t1_r2, "cars"."updated_at" AS t1_r3, "business_cars_people"."id" AS t2_r0, "business_cars_people"."name" AS t2_r1, "business_cars_people"."created_at" AS t2_r2, "business_cars_people"."updated_at" AS t2_r3, "home_cars_people"."id" AS t3_r0, "home_cars_people"."name" AS t3_r1, "home_cars_people"."created_at" AS t3_r2, "home_cars_people"."updated_at" AS t3_r3 FROM "people" LEFT OUTER JOIN "cars" ON "cars"."id" = "people"."favorite_car_id" LEFT OUTER JOIN "cars" "business_cars_people" ON "business_cars_people"."id" = "people"."business_car_id" LEFT OUTER JOIN "cars" "home_cars_people" ON "home_cars_people"."id" = "people"."home_car_id" WHERE (people.id = 1)
=> #<ActiveRecord::Relation [#<Person id: 1, favorite_car_id: 1, business_car_id: 2, home_car_id: 3, name: "Frankie", created_at: "2014-02-18 21:51:58", updated_at: "2014-02-18 21:53:34">]>

重要提示:Rails 会自动使用people 作为person 的复数形式。所以当你创建一个Person 模型时,它会创建一个people 数据库表。

【讨论】:

  • 看起来不错,但它正在执行三个 LEFT OUTER JOIN。做两个查询不是更好吗?装载人员并装载三辆汽车。
  • @AndréBarbosa 这个问题要求一个查询的解决方案。
  • 左外连接贵吗?
  • 我不是批评,只是想知道是否有性能更好的解决方案。
  • @AndréBarbosa 我认为肯定有更好的解决方案。我可能只会使用您的两个查询,确保我的表已正确索引,然后处理包含此数据的页面的缓存和缓存失效。
【解决方案2】:

有一篇很棒的关于手动 Eager 加载的帖子。

http://mrbrdo.wordpress.com/2013/09/25/manually-preloading-associations-in-rails-using-custom-scopessql/

我想这就是你一直在寻找的:

owners = People.all
association_name = :photos

owners.each do |owner|
   record = whatever_you_want

   association = owner.association(association_name)
   association.target = record
   association.set_inverse_instance(record)
end

【讨论】:

    【解决方案3】:

    我会亲自创建一个连接表。话虽如此,这里还有另一种选择:可以根据 SQL 查询定义关系:

    class Person
      belongs_to :favourite_car, class_name: 'Car'
      belongs_to :business_car, class_name: 'Car'
      belongs_to :home_car, class_name: 'Car'
    
      has_many :cars, class_name: 'Car', :finder_sql => %q(
       SELECT DISTINCT cars.*
       FROM cars
       WHERE cars.id IN (#{c_ids})
      )
    
      def c_ids
        [favourite_car_id, business_car_id, home_car_id].compact.uniq.join(',')
      end
    end
    

    然后你可以做 Person.includes(:cars)

    【讨论】:

    • 这不起作用。它尝试执行查询SELECT "cars".* FROM "cars" WHERE "cars"."person_id" IN (1, 2, 3),其中 1,2,3 是人员 ID。
    • 这很奇怪。我修改了答案。你能再试一次吗?
    猜你喜欢
    • 2010-12-01
    • 2017-08-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-14
    • 2011-03-16
    • 1970-01-01
    相关资源
    最近更新 更多