1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Avoid extraneous preloading when loading across has_one associations

The Preloader relies on other objects to bind the retrieved records to their
parents. When executed across a hash, it assumes that the results of
`preloaded_records` is the appropriate set of records to pass in to the next
layer.

Filtering based on the reflection properties in `preloaded_records` allows us to
avoid excessive preloading in the instance where we are loading across a
`has_one` association distinguished by an order (e.g. "last comment" or
similar), by dropping these records before they are returned to the
Preloader. In this situation, we avoid potentially very long key lists in
generated queries and the consequential AR object instantiations.

This is mostly relevant if the underlying linked set has relatively many
records, because this is effectively a multiplier on the number of records
returned on the far side of the preload. Unfortunately, avoiding the
over-retrieval of the `has_one` association seems to require substantial changes
to the preloader design, and probably adaptor-specific logic -- it is a
top-by-group problem.
This commit is contained in:
Michael Fowler 2019-12-29 16:40:16 +13:00
parent 11781d0632
commit 9aa59f9d4a
3 changed files with 63 additions and 1 deletions

View file

@ -38,7 +38,24 @@ module ActiveRecord
def preloaded_records def preloaded_records
return @preloaded_records if defined?(@preloaded_records) return @preloaded_records if defined?(@preloaded_records)
@preloaded_records = owner_keys.empty? ? [] : records_for(owner_keys)
raw_records = owner_keys.empty? ? [] : records_for(owner_keys)
seen_records_by_owner = {}.compare_by_identity
@preloaded_records = raw_records.select do |record|
assignments = []
owners_by_key[convert_key(record[association_key_name])].each do |owner|
entries = (seen_records_by_owner[owner] ||= [])
if reflection.collection? || entries.empty?
entries << record
assignments << record
end
end
!assignments.empty?
end
end end
private private

View file

@ -200,4 +200,46 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
assert_equal expected, actual assert_equal expected, actual
end end
def test_preloading_across_has_one_constrains_loaded_records
author = authors(:david)
old_post = author.posts.create!(title: "first post", body: "test")
old_post.comments.create!(author: authors(:mary), body: "a response")
recent_post = author.posts.create!(title: "first post", body: "test")
last_comment = recent_post.comments.create!(author: authors(:bob), body: "a response")
authors = Author.where(id: author.id)
retrieved_comments = []
reset_callbacks(Comment, :initialize) do
Comment.after_initialize { |record| retrieved_comments << record }
authors.preload(recent_post: :comments).load
end
assert_equal 1, retrieved_comments.size
assert_equal [last_comment], retrieved_comments
end
def test_preloading_across_has_one_through_constrains_loaded_records
author = authors(:david)
old_post = author.posts.create!(title: "first post", body: "test")
old_post.comments.create!(author: authors(:mary), body: "a response")
recent_post = author.posts.create!(title: "first post", body: "test")
recent_post.comments.create!(author: authors(:bob), body: "a response")
authors = Author.where(id: author.id)
retrieved_authors = []
reset_callbacks(Author, :initialize) do
Author.after_initialize { |record| retrieved_authors << record }
authors.preload(recent_response: :author).load
end
assert_equal 2, retrieved_authors.size
assert_equal [author, authors(:bob)], retrieved_authors
end
end end

View file

@ -160,6 +160,9 @@ class Author < ActiveRecord::Base
has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
has_many :posts_mentioning_author, ->(record = nil) { where("posts.body LIKE ?", "%#{record&.name&.downcase}%") }, class_name: "Post" has_many :posts_mentioning_author, ->(record = nil) { where("posts.body LIKE ?", "%#{record&.name&.downcase}%") }, class_name: "Post"
has_one :recent_post, -> { order(id: :desc) }, class_name: "Post"
has_one :recent_response, through: :recent_post, source: :comments
has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do
def extension_method; end def extension_method; end
end end