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

@ -38,7 +38,24 @@ def records_by_owner
def 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
private

@ -200,4 +200,46 @@ def test_preloaded_records_are_not_duplicated
assert_equal expected, actual
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

@ -160,6 +160,9 @@ def ratings
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_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
def extension_method; end
end