【问题标题】:Is there a way to preload an arbitrary number of parent associations in Rails?有没有办法在 Rails 中预加载任意数量的父关联?
【发布时间】:2018-11-03 16:54:07
【问题描述】:

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

该父实例也可以属于另一个更高级别的父实例,它的父实例也可以属于其他级别的父实例,等等。大象,一直到下。

我想做的是将 Location 的名称及其所有父实例串在一起,所以我最终会得到类似 "Top-level group &gt; Mid-level group &gt; Lowest group &gt; 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 问题)。一旦我在一个页面中包含多个位置,这是一个很多 减慢负载的查询。我想像普通的belongs_to 关联一样预加载(通过includes)这个结构,但我不知道是否有办法像这样包含任意数量的父母。

有吗?我该怎么做?

【问题讨论】:

  • 我不认为有办法做到这一点,也许如果你使用像 Mongo 这样的东西,你可以将文档嵌入到文档中,并且一次加载所有内容,但我猜你使用的是 SQL 类型的数据库。我想您可以尝试添加一个带有模型父 ID 的新列,以便通过一个查询将所有这些列都包含在内,但是您必须在任何更改时更新该列,这是防止 N+1 问题的一种方法。
  • 有趣的想法,@arieljuod - 这需要高性能,所以我必须检查每次更改的更新是否可行,但似乎不会增加太多加载。 (是的,您对 DB 的看法是正确的——我使用的是 MySQL)。
  • 如果您使用的是 PostgreSQL,您可以使用递归 CTE(即WITH RECURSIVE)以递归方式将表连接到自身。 AFAIK 有一些方法可以在 MySQL 中模拟递归 CTE,但我不知道它们是什么。

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


【解决方案1】:

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

【讨论】:

  • 您已将这些预加载方法定义为类方法 - 我假设我可以通过声明 records = [self]? 来有效地将它们重新定义为实例方法?
  • 是的,实例方法是self.class.deep_preload_group([self])
  • 太棒了,谢谢。我想我不需要对它的返回值做任何事情?
  • 你是对的。 Rails 的preload 返回预加载器,但您可以选择返回的内容以防需要链接。 records 参数非常适合作为返回值。
  • 确认工作。我也做了一些概括,以便它适用于任何关联,并且它可以用于引用的表以及当前表。这是一个要点,如果你有兴趣:gist.github.com/ArtOfCode-/8efd6417cdad7baa7c663f9f11edbf6d
猜你喜欢
  • 1970-01-01
  • 2011-06-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-03
  • 2011-02-17
相关资源
最近更新 更多