Fix cache_belongs_to prefetch with owners sharing a foreign key - #635
Open
VolodyaAll wants to merge 1 commit into
Open
VolodyaAll wants to merge 1 commit into
VolodyaAll wants to merge 1 commit into
Conversation
Cached::BelongsTo#fetch_async indexed the owners to load by foreign key with a plain hash, so when several owners pointed at the same record only the last one kept its entry. The prefetched record was written into that owner only, and every other owner fell back to fetch_by_id, one cache round trip each. Collect every owner per foreign key and write the record into all of them, as the polymorphic branch already does. The collection yielded to nested includes is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
|
I have signed the CLA! |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In the non-polymorphic branch of
Cached::BelongsTo#fetch_async, the owners to load are indexed by foreign key with a plain hash:When several owners share the same foreign key, each one overwrites the previous entry, so the prefetched record is written only into the last owner. The other owners stay unloaded, and their
fetch_<association>falls back tofetch_by_id, which costs one extra cache round trip per owner. Prefetching quietly degrades into N+1 cache reads. Includes nested under the association are lost for those owners too, because the fallback returns a separate copy of the record that doesn't have them.This is easy to hit with nested includes, where many children belong to the same record:
Fix
Collect all owners per foreign key and write the associated record into each of them. The polymorphic branch of the same method already iterates over every owner and assigns the record to each one, so the two branches now behave the same. The collection yielded to nested includes stays unchanged: unique associated records with
nils compacted.Tests
test_prefetch_associations_cached_belongs_to_with_shared_foreign_keycovers two owners with the same foreign key, prefetched with a symbol include (LoadStrategy::Eager). Without the fix,fetch_itemon the first owner does an extra cache operation (1 instead of 0).test_fetch_multi_batch_fetches_non_embedded_second_level_belongs_to_associations_with_shared_foreign_keycovers the nested includes case above (LoadStrategy::Lazy). Without the fix it does 5 cache operations instead of 3.I ran
rake testlocally against MySQL 5.7 with dalli and against PostgreSQL with memcached_store, thenrake test_allandrubocopwith the latest-release gemfile. Everything passes except two tests on the Rails edge gemfile (FetchTest#test_fetch_by_title_hitandIndexCacheTest#test_fetch_with_unique_adds_limit_clause). Those also fail onmainand are unrelated to this change.I also checked the other cached associations for the same pattern.
Reference::HasManyandReference::HasOnekey their hash by child id, and a child belongs to a single parent, so they are not affected.🤖 Generated with Claude Code