1
0
Fork 0
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:
Eileen M. Uchitelle 2020-12-11 09:14:27 -05:00 committed by GitHub
commit 3e5d504f78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 69 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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