ruby-on-rails 使用include扩展定制Rails预加载器

ar5n3qh5  于 2023-02-17  发布在  Ruby
关注(0)|答案(2)|浏览(180)

我想通过#includes调用触发关系的急切加载,这是通过使用Rails 7应用Ruby 3.2上的ActiveRecord::Associations::Preloader的自定义预加载器修改的:

class Product < ApplicationRecord
  belongs_to :vendor
  has_many :taxons
end

class Vendor < ApplicationRecord
  has_many :products
end

records = Product.where(id: [1,2,3,4])
scope = Vendor.where.not(id: [5,6])
preloader = ActiveRecord::Associations::Preloader.new(records:, associations: :vendor, scope:).tap(&:call)

puts records.first.association_cached?(:vendor)
=> true

puts records.includes(:taxons).first.association_cached?(:vendor)
=> false

我需要scope:参数,因为有些用户不允许访问某些资源。
如果没有includes(:taxons)调用,关联:vendor会正确地立即加载和缓存,但在第二种情况下,似乎Preloader的整个立即加载都被丢弃了。
有没有办法将自定义预加载器作为链参数与其他includes沿着包含?当我尝试包含预加载器本身时,我得到了一个错误,如下所示:

records.includes(preloader).to_a
ActiveRecord::AssociationNotFoundError: Association named '#<ActiveRecord::Associations::Preloader:0x00007f573bc31ab0>' was not found on Product; perhaps you misspelled it?
from /usr/local/bundle/gems/activerecord-7.0.4.2/lib/active_record/associations.rb:302:in `association'
5t7ly7z5

5t7ly7z51#

这里发生的事情是ActiveRecord::Associations::Preloader需要初始化对象来填充数据,但是在ActiveRecord::Relation上调用任何查询都将重新加载对象。
records上调用ActiveRecord::Relation的重载方法后,必须调用Preloader。它应该与将自定义预加载器添加到链中没有任何性能差异,因为它仍然必须在每次结束时被调用。
将它添加到链中需要一些猴子修补。它的起点将是ActiveRecord:: Relation#preload_associations,到您必须传递作用域的地方。
如果你只需要数据,先调用includes,然后再调用Preloader。如果你先需要数据而不需要taxons,然后再包含它们,调用第二个自定义预加载器来加载taxons数据。这也会对速度有积极的影响,因为productsvendors不必再次加载和初始化。

records = Product.where(id: [1,2,3,4])
scope = Vendor.where.not(id: [5,6])
ActiveRecord::Associations::Preloader.new(records:, associations: :vendor, scope:).tap(&:call)

# associations filtered by the scope are initialized but return nil, although association_id is present
records.map{|r| r.association_cached?(:vendor)}
=> [true, true, true, true]
records.map{|r| r.association_cached?(:taxons)}
=> [false, false, false, false]

ActiveRecord::Associations::Preloader.new(records:, associations: :taxons).tap(&:call)

records.map{|r| r.association_cached?(:vendor)}
=> [true, true, true, true]
records.map{|r| r.association_cached?(:taxons)}
=> [true, true, true, true]
ct3nt3jp

ct3nt3jp2#

正如Jan Vítek所指出的,在关系上调用的任何查询都将重新加载所有对象,因此会释放所有已经急切加载的关联。
由于代码的结构,我不能保证总是调用定制的ActiveRecord::Associations::Preloader,所以我为ActiveRecord::Relation#preload_associations提供了一个小的monkey补丁:

raise "ActiveRecord::Relation#preload_associations is no longer available, check patch!" unless ActiveRecord::Relation.method_defined? :preload_associations
raise "ActiveRecord::Relation#preload_associations arity != 1, check patch!" unless ActiveRecord::Relation.instance_method(:preload_associations).arity == 1

module PreloadWithScopePatch
  def preload_with_scope(association, scope)
    scope = model.reflect_on_association(association).klass.where(scope) if scope.is_a?(Hash)
    (@preload_with_scope ||= []).push([association, scope])
    self
  end

  def preload_associations(records)
    super.tap do
      Array(@preload_with_scope).each do |associations, scope|
        ActiveRecord::Associations::Preloader.new(records:, associations:, scope:).call
      end
    end
  end
end
ActiveRecord::Relation.prepend(PreloadWithScopePatch)
ActiveRecord::Base.singleton_class.delegate(:preload_with_scope, to: :all)

现在,您可以按如下方式使用它:

records = Product.where(id: [1,2,3,4]).preload_with_scope(:vendor, Vendor.where.not(id: [5,6])).includes(:taxons)

records.map { |r| r.association_cached?(:vendor) }
=> [true, true, true, true]

records.map { |r| r.association_cached?(:taxons) }
=> [true, true, true, true]

相关问题