有没有办法在Rails中预加载任意数量的父级关联?

时间:2018-11-03 16:54:08

标签: mysql ruby-on-rails ruby belongs-to

  

TL; DR::我有一个模型belongs_to :group,其中group是同一模型的另一个实例。该“父母” group也可以有一个父母,依此类推。有没有一种方法可以includes扩展到最远?

我有一个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

换句话说,它可以选择属于其自身的“父”实例,称为group

该父实例也可以属于另一个上级的父实例,例如它的父级也可以属于其他级别。大象一直向下。

我想做的是将位置及其所有父实例的名称串在一起,所以我最终得到了类似"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

但是,唯一的问题是,它将在到达每个父实例时分别查询每个父实例(N + 1问题)。在单个页面中包含多个位置后,这是一个 lot 查询,这会减缓加载速度。我想像通过普通的includes那样预加载(通过belongs_to)此结构,但是我不知道是否有办法包含这样任意数量的父母。 / p>

在吗?我该怎么办?

1 个答案:

答案 0 :(得分:2)

使用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和层次结构的实际深度中的最小值