有没有办法在 Rails 中预加载任意数量的 parent 关联?
Is there a way to preload an arbitrary number of parent associations in Rails?
TL;DR: I have a model that belongs_to :group
, where group
is another instance of the same model. That "parent" group
can also have a parent, and so on up the chain. Is there a way to includes
this structure as far up as it goes?
我有一个 Location
模型,它看起来像这样(简化版):
create_table "locations", force: :cascade do |t|
t.string "name"
t.decimal "lat", precision: 20, scale: 15
t.decimal "long", precision: 20, scale: 15
t.bigint "group_id"
t.string "type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["group_id"], name: "index_locations_on_group_id"
end
class Location < ApplicationRecord
belongs_to :group, class_name: 'Location', required: false
has_many :locations, foreign_key: 'group_id', dependent: :destroy
end
换句话说,它可以选择属于自己的 "parent" 个实例,引用为 group
。
那个 parent 实例也可以属于另一个上层的 parent 实例,它的 parent 也可以,等等。大象,一路向下。
我想做的是将位置的名称及其所有 parent 实例串在一起,所以我最终得到类似 "Top-level group > Mid-level group > Lowest group > Location"
的结果。这很好,我已经在模型中实现了它:
def parent_chain
Enumerator.new do |enum|
parent_group = group
while parent_group != nil
enum.yield parent_group
parent_group = parent_group.group
end
end
end
def name_chain
(parent_chain.map(&:name).reverse + [name]).join(" \u00BB ")
end
然而,唯一的问题是它会在每个 parent 实例到达那里时单独查询(N+1 问题)。一旦我在单个页面中包含多个位置,这是一个 很多 的查询,这会减慢加载速度。我想预加载(通过 includes
)这个结构,就像我对普通 belongs_to
关联所做的那样,但我不知道是否有办法包含任意数量的 parent是这样的。
在吗?我该怎么做?
使用includes
?不。递归预加载可以通过这种方式实现:
解决方案 #1:真正的递归
class Location
belongs_to :group
# Impure method that preloads the :group association on an array of group instances.
def self.preload_group(records)
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(records, :group)
end
# Impure method that recursively preloads the :group association
# until there are no more parents.
# Will trigger an infinite loop if the hierarchy has cycles.
def self.deep_preload_group(records)
return if records.empty?
preload_group(records)
deep_preload_group(records.select(&:group).map(&:group))
end
end
locations = Location.all
Location.deep_preload_group(locations)
查询的数量将是组层次结构的深度。
解决方案 #2:接受层次结构深度限制
class Location
# depth needs to be greather than 1
def self.deep_preload_group(records, depth=10)
to_preload = :group
(depth - 1).times { to_preload = {group: to_preload} }
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(records, to_preload)
end
end
查询数量将是 depth
和实际层次结构深度
中的最小值
TL;DR: I have a model that
belongs_to :group
, wheregroup
is another instance of the same model. That "parent"group
can also have a parent, and so on up the chain. Is there a way toincludes
this structure as far up as it goes?
我有一个 Location
模型,它看起来像这样(简化版):
create_table "locations", force: :cascade do |t|
t.string "name"
t.decimal "lat", precision: 20, scale: 15
t.decimal "long", precision: 20, scale: 15
t.bigint "group_id"
t.string "type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["group_id"], name: "index_locations_on_group_id"
end
class Location < ApplicationRecord
belongs_to :group, class_name: 'Location', required: false
has_many :locations, foreign_key: 'group_id', dependent: :destroy
end
换句话说,它可以选择属于自己的 "parent" 个实例,引用为 group
。
那个 parent 实例也可以属于另一个上层的 parent 实例,它的 parent 也可以,等等。大象,一路向下。
我想做的是将位置的名称及其所有 parent 实例串在一起,所以我最终得到类似 "Top-level group > Mid-level group > Lowest group > Location"
的结果。这很好,我已经在模型中实现了它:
def parent_chain
Enumerator.new do |enum|
parent_group = group
while parent_group != nil
enum.yield parent_group
parent_group = parent_group.group
end
end
end
def name_chain
(parent_chain.map(&:name).reverse + [name]).join(" \u00BB ")
end
然而,唯一的问题是它会在每个 parent 实例到达那里时单独查询(N+1 问题)。一旦我在单个页面中包含多个位置,这是一个 很多 的查询,这会减慢加载速度。我想预加载(通过 includes
)这个结构,就像我对普通 belongs_to
关联所做的那样,但我不知道是否有办法包含任意数量的 parent是这样的。
在吗?我该怎么做?
使用includes
?不。递归预加载可以通过这种方式实现:
解决方案 #1:真正的递归
class Location
belongs_to :group
# Impure method that preloads the :group association on an array of group instances.
def self.preload_group(records)
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(records, :group)
end
# Impure method that recursively preloads the :group association
# until there are no more parents.
# Will trigger an infinite loop if the hierarchy has cycles.
def self.deep_preload_group(records)
return if records.empty?
preload_group(records)
deep_preload_group(records.select(&:group).map(&:group))
end
end
locations = Location.all
Location.deep_preload_group(locations)
查询的数量将是组层次结构的深度。
解决方案 #2:接受层次结构深度限制
class Location
# depth needs to be greather than 1
def self.deep_preload_group(records, depth=10)
to_preload = :group
(depth - 1).times { to_preload = {group: to_preload} }
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(records, to_preload)
end
end
查询数量将是 depth
和实际层次结构深度