Merge pull request #41790 from jhawthorn/preloader_smart_batching

"Smart" ActiveRecord Preloader batching
This commit is contained in:
John Hawthorn 2021-04-05 10:14:07 -07:00 committed by GitHub
commit 0ff395e1b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 11 deletions

View File

@ -40,6 +40,8 @@ module ActiveRecord
end
end
attr_reader :klass
def initialize(klass, owners, reflection, preload_scope, associate_by_default = true)
@klass = klass
@owners = owners.uniq(&:__id__)
@ -50,8 +52,20 @@ module ActiveRecord
@run = false
end
def already_loaded?
@already_loaded ||= owners.all? { |o| o.association(reflection.name).loaded? }
def table_name
@klass.table_name
end
def data_available?
already_loaded?
end
def future_classes
if run? || already_loaded?
[]
else
[@klass]
end
end
def runnable_loaders
@ -149,7 +163,11 @@ module ActiveRecord
end
private
attr_reader :owners, :reflection, :preload_scope, :model, :klass
attr_reader :owners, :reflection, :preload_scope, :model
def already_loaded?
@already_loaded ||= owners.all? { |o| o.association(reflection.name).loaded? }
end
def fetch_from_preloaded_records
@records_by_owner = owners.index_with do |owner|

View File

@ -13,11 +13,20 @@ module ActiveRecord
until branches.empty?
loaders = branches.flat_map(&:runnable_loaders)
already_loaded, loaders = loaders.partition(&:already_loaded?)
already_loaded.each(&:run)
already_loaded = loaders.select(&:data_available?)
if already_loaded.any?
already_loaded.each(&:run)
elsif loaders.any?
future_tables = branches.flat_map do |branch|
branch.future_classes - branch.runnable_loaders.map(&:klass)
end.map(&:table_name).uniq
group_and_load_similar(loaders)
loaders.each(&:run)
target_loaders = loaders.reject { |l| future_tables.include?(l.table_name) }
target_loaders = loaders if target_loaders.empty?
group_and_load_similar(target_loaders)
target_loaders.each(&:run)
end
finished, in_progress = branches.partition(&:done?)

View File

@ -15,6 +15,40 @@ module ActiveRecord
@associate_by_default = associate_by_default
@children = build_children(children)
@loaders = nil
end
def future_classes
(immediate_future_classes + children.flat_map(&:future_classes)).uniq
end
def immediate_future_classes
if parent.done?
loaders.flat_map(&:future_classes).uniq
else
likely_reflections.reject(&:polymorphic?).flat_map do |reflection|
reflection.
chain.
map(&:klass)
end.uniq
end
end
def target_classes
if done?
preloaded_records.map(&:klass).uniq
elsif parent.done?
loaders.map(&:klass).uniq
else
likely_reflections.reject(&:polymorphic?).map(&:klass).uniq
end
end
def likely_reflections
parent_classes = parent.target_classes
parent_classes.map do |parent_klass|
parent_klass._reflect_on_association(@association)
end.compact
end
def root?
@ -30,7 +64,7 @@ module ActiveRecord
end
def done?
loaders.all?(&:run?)
root? || (@loaders && @loaders.all?(&:run?))
end
def runnable_loaders

View File

@ -35,16 +35,37 @@ module ActiveRecord
end
end
def data_available?
return true if super()
through_preloaders.all?(&:run?) &&
source_preloaders.all?(&:run?)
end
def runnable_loaders
if already_loaded?
if data_available?
[self]
elsif through_preloaders.all?(&:run?)
[self] + source_preloaders.flat_map(&:runnable_loaders)
source_preloaders.flat_map(&:runnable_loaders)
else
through_preloaders.flat_map(&:runnable_loaders)
end
end
def future_classes
if run? || data_available?
[]
elsif through_preloaders.all?(&:run?)
source_preloaders.flat_map(&:future_classes).uniq
else
through_classes = through_preloaders.flat_map(&:future_classes)
source_classes = source_reflection.
chain.
reject { |reflection| reflection.respond_to?(:polymorphic?) && reflection.polymorphic? }.
map(&:klass)
(through_classes + source_classes).uniq
end
end
private
def source_preloaders
@source_preloaders ||= ActiveRecord::Associations::Preloader.new(records: middle_records, associations: source_reflection.name, scope: scope, associate_by_default: false).loaders

View File

@ -362,7 +362,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
end
class PreloaderTest < ActiveRecord::TestCase
fixtures :posts, :comments, :books, :authors
fixtures :posts, :comments, :books, :authors, :tags, :taggings
def test_preload_with_scope
post = posts(:welcome)
@ -569,6 +569,40 @@ class PreloaderTest < ActiveRecord::TestCase
end
end
def test_preload_can_group_separate_levels
mary = authors(:mary)
bob = authors(:bob)
AuthorFavorite.create!(author: mary, favorite_author: bob)
assert_queries(3) do
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: [:posts, favorite_authors: :posts])
preloader.call
end
assert_no_queries do
mary.posts
mary.favorite_authors.map(&:posts)
end
end
def test_preload_can_group_multi_level_ping_pong_through
mary = authors(:mary)
bob = authors(:bob)
AuthorFavorite.create!(author: mary, favorite_author: bob)
assert_queries(9) do
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: { similar_posts: :comments, favorite_authors: { similar_posts: :comments } })
preloader.call
end
assert_no_queries do
mary.similar_posts.map(&:comments).each(&:to_a)
mary.favorite_authors.flat_map(&:similar_posts).map(&:comments).each(&:to_a)
end
end
def test_preload_does_not_group_same_class_different_scope
post = posts(:welcome)
postesque = Postesque.create(author: Author.last)