Есть ли способ предварительно загрузить произвольное количество родительских ассоциаций в Rails? - PullRequest
0 голосов
/ 03 ноября 2018

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). Как только я включаю несколько локаций на одной странице, это много запросов, замедляющих загрузку. Я хотел бы предварительно загрузить (через includes) эту структуру, как для обычной belongs_to ассоциации, но я не знаю, есть ли способ включить произвольное число родителей, подобных этому.

Есть? Как мне это сделать?

1 Ответ

0 голосов
/ 03 ноября 2018

Используя 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 и фактическая глубина иерархии

...