From f5f48cb8a524a6f1e66557ce4e29cdaf4376a7ca Mon Sep 17 00:00:00 2001 From: Dustin Brown Date: Wed, 13 Dec 2023 21:45:36 +0000 Subject: [PATCH] Prevent duplicate records when preloading has_many When preloading a has_many association, we were simply concatenating the preloaded records without regard to whether they were already loaded on the owner. Even though there is a check for `loaded?` in this part of the preloader, some persisted records may not be marked as such. For example, if a record is created via `owner.association.create`. This change reverts to the previous behavior of replacing the target association while also preserving non-persisted records, which was the goal of https://github.com/rails/rails/pull/50129. Co-authored-by: John Hawthorn --- .../associations/preloader/association.rb | 4 ++-- activerecord/test/cases/associations_test.rb | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 4db5c67ad0f..7c23b7fbe26 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -247,8 +247,8 @@ module ActiveRecord association = owner.association(reflection.name) if reflection.collection? - association.loaded! - association.target.concat(records) + not_persisted_records = association.target.reject(&:persisted?) + association.target = records + not_persisted_records else association.target = records.first end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 69c1adfd436..812a6f17030 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -804,6 +804,17 @@ class PreloaderTest < ActiveRecord::TestCase end end + def test_preload_does_not_concatenate_duplicate_records + post = posts(:welcome) + post.reload + post.comments.create!(body: "A new comment") + + ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments).call + + assert_equal post.comments.length, post.comments.count + assert_equal post.comments.all.to_a, post.comments + end + def test_preload_for_hmt_with_conditions post = posts(:welcome) _normal_category = post.categories.create!(name: "Normal")