From 6f6c441662e73084087da3f564e71f0a174c3ee4 Mon Sep 17 00:00:00 2001 From: Justin Carvalho Date: Wed, 13 Oct 2021 11:42:39 -0400 Subject: [PATCH] fix: duplicate active record objects on inverse_of Fixes a bug that causes duplicate references to the same object to be added to the collection association on an active_record object when inverse_of is used. In cases where has_many_inversing is set to true, the set_inverse_instance function calls target= on collection_association during concat resulting in multiple appends to target. This only occurs for new records. This PR introduces changes that sets the index so the duplicate object replaces the original. Closes issue #43222. Co-authored-by: Terence Li Co-authored-by: Dave Rose --- .../active_record/associations/collection_association.rb | 4 ++++ .../test/cases/associations/inverse_associations_test.rb | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 54597b6e58d2c..d5fe12b43d56e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -456,6 +456,10 @@ def replace_on_target(record, skip_callbacks, replace:, inversing: false) yield(record) if block_given? + if !index && @replaced_or_added_targets.include?(record) + index = @target.index(record) + end + @replaced_or_added_targets << record if inversing || index || record.new_record? if index diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 0bb1470981767..f53b11da13de9 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -766,6 +766,15 @@ def test_with_has_many_inversing_does_not_trigger_association_callbacks_on_set_w end end + def test_with_hash_many_inversing_does_not_add_duplicate_associated_objects + with_has_many_inversing(Interest) do + human = Human.new + interest = Interest.new(human: human) + human.interests << interest + assert_equal 1, human.interests.size + end + end + def test_unscope_does_not_set_inverse_when_incorrect interest = interests(:trainspotting) human = interest.human