[Fix #50897] Autosaving has_one sets foreign key attribute when unchanged

This commit is contained in:
Joshua Young 2024-01-28 19:23:33 +10:00
parent 3528b9df8b
commit a518b5a9d9
4 changed files with 38 additions and 7 deletions

@ -1,3 +1,10 @@
* Fix `has_one` association autosave setting the foreign key attribute when it is unchanged.
This behaviour is also inconsistent with autosaving `belongs_to` and can have unintended side effects like raising
an `ActiveRecord::ReadOnlyAttributeError` when the foreign key attribute is marked as read-only.
*Joshua Young*
* Remove deprecated behavior that would rollback a transaction block when exited using `return`, `break` or `throw`.
*Rafael Mendonça França*

@ -458,7 +458,8 @@ def save_has_one_association(reflection)
primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
primary_key_foreign_key_pairs.each do |primary_key, foreign_key|
record[foreign_key] = _read_attribute(primary_key)
association_id = _read_attribute(primary_key)
record[foreign_key] = association_id unless record[foreign_key] == association_id
end
association.set_inverse_instance(record)
end

@ -302,6 +302,13 @@ def test_callbacks_firing_order_on_save
eye.update(iris_attributes: { color: "blue" })
assert_equal [false, false, false, false], eye.after_save_callbacks_stack
end
def test_foreign_key_attribute_is_not_set_unless_changed
eye = Eye.create!(iris_with_read_only_foreign_key_attributes: { color: "honey" })
assert_nothing_raised do
eye.update!(override_iris_with_read_only_foreign_key_color: true)
end
end
end
class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase

@ -4,19 +4,20 @@ class Eye < ActiveRecord::Base
attr_reader :after_create_callbacks_stack
attr_reader :after_update_callbacks_stack
attr_reader :after_save_callbacks_stack
attr_writer :override_iris_with_read_only_foreign_key_color
# Callbacks configured before the ones has_one sets up.
after_create :trace_after_create
after_update :trace_after_update
after_save :trace_after_save
after_create :trace_after_create, if: :iris
after_update :trace_after_update, if: :iris
after_save :trace_after_save, if: :iris
has_one :iris
accepts_nested_attributes_for :iris
# Callbacks configured after the ones has_one sets up.
after_create :trace_after_create2
after_update :trace_after_update2
after_save :trace_after_save2
after_create :trace_after_create2, if: :iris
after_update :trace_after_update2, if: :iris
after_save :trace_after_save2, if: :iris
def trace_after_create
(@after_create_callbacks_stack ||= []) << !iris.persisted?
@ -32,8 +33,23 @@ def trace_after_save
(@after_save_callbacks_stack ||= []) << iris.has_changes_to_save?
end
alias trace_after_save2 trace_after_save
has_one :iris_with_read_only_foreign_key, class_name: "IrisWithReadOnlyForeignKey", foreign_key: :eye_id
accepts_nested_attributes_for :iris_with_read_only_foreign_key
before_save :set_iris_with_read_only_foreign_key_color_to_blue, if: -> {
iris_with_read_only_foreign_key && @override_iris_with_read_only_foreign_key_color
}
def set_iris_with_read_only_foreign_key_color_to_blue
iris_with_read_only_foreign_key.color = "blue"
end
end
class Iris < ActiveRecord::Base
belongs_to :eye
end
class IrisWithReadOnlyForeignKey < Iris
attr_readonly :eye_id
end