mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #40776 from eileencodes/refactor-preloader-part-1
Refactor ActiveRecord::Associations::Preloader class
This commit is contained in:
commit
3e5d504f78
5 changed files with 60 additions and 69 deletions
|
@ -41,7 +41,6 @@ module ActiveRecord
|
|||
#
|
||||
# This could result in many rows that contain redundant data and it performs poorly at scale
|
||||
# and is therefore only used when necessary.
|
||||
#
|
||||
class Preloader #:nodoc:
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
|
@ -50,6 +49,8 @@ module ActiveRecord
|
|||
autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
|
||||
end
|
||||
|
||||
attr_reader :records, :associations, :scope, :associate_by_default, :polymorphic_parent
|
||||
|
||||
# Eager loads the named associations for the given Active Record record(s).
|
||||
#
|
||||
# In this description, 'association name' shall refer to the name passed
|
||||
|
@ -84,75 +85,61 @@ module ActiveRecord
|
|||
# [ :books, :author ]
|
||||
# { author: :avatar }
|
||||
# [ :books, { author: :avatar } ]
|
||||
def preload(records, associations, preload_scope = nil)
|
||||
records = Array.wrap(records).compact
|
||||
|
||||
if records.empty?
|
||||
[]
|
||||
def initialize(associate_by_default: true, polymorphic_parent: false, **kwargs)
|
||||
if kwargs.empty?
|
||||
ActiveSupport::Deprecation.warn("Calling `Preloader#initialize` without arguments is deprecated and will be removed in Rails 7.0.")
|
||||
else
|
||||
Array.wrap(associations).flat_map { |association|
|
||||
preloaders_on association, records, preload_scope
|
||||
}
|
||||
@records = kwargs[:records]
|
||||
@associations = kwargs[:associations]
|
||||
@scope = kwargs[:scope]
|
||||
@associate_by_default = associate_by_default
|
||||
@polymorphic_parent = polymorphic_parent
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(associate_by_default: true)
|
||||
@associate_by_default = associate_by_default
|
||||
def call
|
||||
return [] if records.empty? || associations.nil?
|
||||
|
||||
build_preloaders
|
||||
end
|
||||
|
||||
def preload(records, associations, preload_scope)
|
||||
ActiveSupport::Deprecation.warn("`preload` is deprecated and will be removed in Rails 7.0. Call `Preloader.new(kwargs).call` instead.")
|
||||
|
||||
Preloader.new(records: records, associations: associations, scope: preload_scope).call
|
||||
end
|
||||
|
||||
private
|
||||
# Loads all the given data into +records+ for the +association+.
|
||||
def preloaders_on(association, records, scope, polymorphic_parent = false)
|
||||
case association
|
||||
when Hash
|
||||
preloaders_for_hash(association, records, scope, polymorphic_parent)
|
||||
when Symbol, String
|
||||
preloaders_for_one(association, records, scope, polymorphic_parent)
|
||||
else
|
||||
raise ArgumentError, "#{association.inspect} was not recognized for preload"
|
||||
end
|
||||
end
|
||||
def build_preloaders
|
||||
Array.wrap(associations).flat_map { |association|
|
||||
Array(association).flat_map { |parent, child|
|
||||
grouped_records(parent).flat_map do |reflection, reflection_records|
|
||||
loaders = preloaders_for_reflection(reflection, reflection_records)
|
||||
|
||||
def preloaders_for_hash(association, records, scope, polymorphic_parent)
|
||||
association.flat_map { |parent, child|
|
||||
grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
|
||||
loaders = preloaders_for_reflection(reflection, reflection_records, scope)
|
||||
recs = loaders.flat_map(&:preloaded_records).uniq
|
||||
child_polymorphic_parent = reflection && reflection.options[:polymorphic]
|
||||
loaders.concat Array.wrap(child).flat_map { |assoc|
|
||||
preloaders_on assoc, recs, scope, child_polymorphic_parent
|
||||
}
|
||||
loaders
|
||||
end
|
||||
if child
|
||||
loaders.concat build_child_preloader(reflection, child, loaders)
|
||||
end
|
||||
|
||||
loaders
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# Loads all the given data into +records+ for a singular +association+.
|
||||
#
|
||||
# Functions by instantiating a preloader class such as Preloader::Association and
|
||||
# call the +run+ method for each passed in class in the +records+ argument.
|
||||
#
|
||||
# Not all records have the same class, so group then preload group on the reflection
|
||||
# itself so that if various subclass share the same association then we do not split
|
||||
# them unnecessarily
|
||||
#
|
||||
# Additionally, polymorphic belongs_to associations can have multiple associated
|
||||
# classes, depending on the polymorphic_type field. So we group by the classes as
|
||||
# well.
|
||||
def preloaders_for_one(association, records, scope, polymorphic_parent)
|
||||
grouped_records(association, records, polymorphic_parent)
|
||||
.flat_map do |reflection, reflection_records|
|
||||
preloaders_for_reflection reflection, reflection_records, scope
|
||||
end
|
||||
def build_child_preloader(reflection, child, loaders)
|
||||
child_polymorphic_parent = reflection && reflection.options[:polymorphic]
|
||||
preloaded_records = loaders.flat_map(&:preloaded_records).uniq
|
||||
|
||||
Preloader.new(records: preloaded_records, associations: child, scope: scope, associate_by_default: associate_by_default, polymorphic_parent: child_polymorphic_parent).call
|
||||
end
|
||||
|
||||
def preloaders_for_reflection(reflection, records, scope)
|
||||
records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
|
||||
preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope, @associate_by_default).run
|
||||
def preloaders_for_reflection(reflection, reflection_records)
|
||||
reflection_records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
|
||||
preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope, associate_by_default).run
|
||||
end
|
||||
end
|
||||
|
||||
def grouped_records(association, records, polymorphic_parent)
|
||||
def grouped_records(association)
|
||||
h = {}
|
||||
records.each do |record|
|
||||
reflection = record.class._reflect_on_association(association)
|
||||
|
|
|
@ -4,8 +4,6 @@ module ActiveRecord
|
|||
module Associations
|
||||
class Preloader
|
||||
class ThroughAssociation < Association # :nodoc:
|
||||
PRELOADER = ActiveRecord::Associations::Preloader.new(associate_by_default: false)
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
@already_loaded = owners.first.association(through_reflection.name).loaded?
|
||||
|
@ -44,7 +42,7 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
def source_preloaders
|
||||
@source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
|
||||
@source_preloaders ||= ActiveRecord::Associations::Preloader.new(records: middle_records, associations: source_reflection.name, scope: scope, associate_by_default: false).call
|
||||
end
|
||||
|
||||
def middle_records
|
||||
|
@ -52,7 +50,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def through_preloaders
|
||||
@through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
|
||||
@through_preloaders ||= ActiveRecord::Associations::Preloader.new(records: owners, associations: through_reflection.name, scope: through_scope, associate_by_default: false).call
|
||||
end
|
||||
|
||||
def through_reflection
|
||||
|
|
|
@ -763,11 +763,9 @@ module ActiveRecord
|
|||
def preload_associations(records) # :nodoc:
|
||||
preload = preload_values
|
||||
preload += includes_values unless eager_loading?
|
||||
preloader = nil
|
||||
scope = strict_loading_value ? StrictLoadingScope : nil
|
||||
preload.each do |associations|
|
||||
preloader ||= build_preloader
|
||||
preloader.preload records, associations, scope
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: associations, scope: scope).call
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -869,10 +867,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def build_preloader
|
||||
ActiveRecord::Associations::Preloader.new
|
||||
end
|
||||
|
||||
def references_eager_loaded_tables?
|
||||
joined_tables = build_joins([]).flat_map do |join|
|
||||
if join.is_a?(Arel::Nodes::StringJoin)
|
||||
|
|
|
@ -1530,10 +1530,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
test "preload with invalid argument" do
|
||||
exception = assert_raises(ArgumentError) do
|
||||
exception = assert_raises(ActiveRecord::AssociationNotFoundError) do
|
||||
Author.preload(10).to_a
|
||||
end
|
||||
assert_equal("10 was not recognized for preload", exception.message)
|
||||
assert_match(/Association named '10' was not found on Author; perhaps you misspelled it\?/, exception.message)
|
||||
end
|
||||
|
||||
test "associations with extensions are not instance dependent" do
|
||||
|
|
|
@ -361,8 +361,20 @@ class PreloaderTest < ActiveRecord::TestCase
|
|||
def test_preload_with_scope
|
||||
post = posts(:welcome)
|
||||
|
||||
preloader = ActiveRecord::Associations::Preloader.new
|
||||
preloader.preload([post], :comments, Comment.where(body: "Thank you for the welcome"))
|
||||
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments, scope: Comment.where(body: "Thank you for the welcome"))
|
||||
preloader.call
|
||||
|
||||
assert_predicate post.comments, :loaded?
|
||||
assert_equal [comments(:greetings)], post.comments
|
||||
end
|
||||
|
||||
def test_legacy_preload_with_scope
|
||||
post = posts(:welcome)
|
||||
|
||||
assert_deprecated do
|
||||
preloader = ActiveRecord::Associations::Preloader.new
|
||||
preloader.preload([post], :comments, Comment.where(body: "Thank you for the welcome"))
|
||||
end
|
||||
|
||||
assert_predicate post.comments, :loaded?
|
||||
assert_equal [comments(:greetings)], post.comments
|
||||
|
|
Loading…
Reference in a new issue