Skip to content

Commit 77019de

Browse files
authored
Merge pull request rails#52412 from joshuay03/alternative-fix-for-48688
More robust fix for duplicate callbacks when singular child autosaves parent
2 parents 0db535f + 44dde3a commit 77019de

File tree

2 files changed

+55
-20
lines changed

2 files changed

+55
-20
lines changed

activerecord/lib/active_record/autosave_association.rb

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,10 @@ def define_autosave_validation_callbacks(reflection)
221221
if reflection.validate? && !method_defined?(validation_method)
222222
if reflection.collection?
223223
method = :validate_collection_association
224+
elsif reflection.has_one?
225+
method = :validate_has_one_association
224226
else
225-
method = :validate_single_association
227+
method = :validate_belongs_to_association
226228
end
227229

228230
define_non_cyclic_method(validation_method) { send(method, reflection) }
@@ -274,6 +276,16 @@ def changed_for_autosave?
274276
new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave?
275277
end
276278

279+
def validating_belongs_to_for?(association)
280+
@validating_belongs_to_for ||= {}
281+
@validating_belongs_to_for[association]
282+
end
283+
284+
def autosaving_belongs_to_for?(association)
285+
@autosaving_belongs_to_for ||= {}
286+
@autosaving_belongs_to_for[association]
287+
end
288+
277289
private
278290
def init_internals
279291
super
@@ -313,18 +325,35 @@ def nested_records_changed_for_autosave?
313325
end
314326

315327
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
316-
# turned on for the association.
317-
def validate_single_association(reflection)
328+
# turned on for the has_one association.
329+
def validate_has_one_association(reflection)
318330
association = association_instance_get(reflection.name)
319331
record = association && association.reader
320332
return unless record && (record.changed_for_autosave? || custom_validation_context?)
321333

322-
inverse_belongs_to_association = inverse_belongs_to_association_for(reflection, record)
323-
return if inverse_belongs_to_association && inverse_belongs_to_association.updated?
334+
inverse_association = reflection.inverse_of && record.association(reflection.inverse_of.name)
335+
return if inverse_association && (record.validating_belongs_to_for?(inverse_association) ||
336+
record.autosaving_belongs_to_for?(inverse_association))
324337

325338
association_valid?(association, record)
326339
end
327340

341+
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
342+
# turned on for the belongs_to association.
343+
def validate_belongs_to_association(reflection)
344+
association = association_instance_get(reflection.name)
345+
record = association && association.reader
346+
return unless record && (record.changed_for_autosave? || custom_validation_context?)
347+
348+
begin
349+
@validating_belongs_to_for ||= {}
350+
@validating_belongs_to_for[association] = true
351+
association_valid?(association, record)
352+
ensure
353+
@validating_belongs_to_for[association] = false
354+
end
355+
end
356+
328357
# Validate the associated records if <tt>:validate</tt> or
329358
# <tt>:autosave</tt> is turned on for the association specified by
330359
# +reflection+.
@@ -443,13 +472,7 @@ def save_has_one_association(reflection)
443472
elsif autosave != false
444473
primary_key = Array(compute_primary_key(reflection, self)).map(&:to_s)
445474
primary_key_value = primary_key.map { |key| _read_attribute(key) }
446-
447-
return unless (autosave && record.changed_for_autosave?) || (record_changed = _record_changed?(reflection, record, primary_key_value))
448-
449-
unless record_changed
450-
inverse_belongs_to_association = inverse_belongs_to_association_for(reflection, record)
451-
return if inverse_belongs_to_association && inverse_belongs_to_association.updated?
452-
end
475+
return unless (autosave && record.changed_for_autosave?) || _record_changed?(reflection, record, primary_key_value)
453476

454477
unless reflection.through_reflection
455478
foreign_key = Array(reflection.foreign_key)
@@ -462,19 +485,15 @@ def save_has_one_association(reflection)
462485
association.set_inverse_instance(record)
463486
end
464487

488+
inverse_association = reflection.inverse_of && record.association(reflection.inverse_of.name)
489+
return if inverse_association && record.autosaving_belongs_to_for?(inverse_association)
490+
465491
saved = record.save(validate: !autosave)
466492
raise ActiveRecord::Rollback if !saved && autosave
467493
saved
468494
end
469495
end
470496

471-
def inverse_belongs_to_association_for(reflection, record)
472-
!reflection.belongs_to? &&
473-
reflection.inverse_of &&
474-
reflection.inverse_of.belongs_to? &&
475-
record.association(reflection.inverse_of.name)
476-
end
477-
478497
# If the record is new or it has changed, returns true.
479498
def _record_changed?(reflection, record, key)
480499
record.new_record? ||
@@ -515,7 +534,15 @@ def save_belongs_to_association(reflection)
515534
foreign_key.each { |key| self[key] = nil }
516535
record.destroy
517536
elsif autosave != false
518-
saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
537+
saved = if record.new_record? || (autosave && record.changed_for_autosave?)
538+
begin
539+
@autosaving_belongs_to_for ||= {}
540+
@autosaving_belongs_to_for[association] = true
541+
record.save(validate: !autosave)
542+
ensure
543+
@autosaving_belongs_to_for[association] = false
544+
end
545+
end
519546

520547
if association.updated?
521548
primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)

activerecord/test/cases/autosave_association_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,14 @@ def test_should_automatically_save_bang_the_associated_model
15741574
assert_equal "The Vile Serpent", @pirate.reload.ship.name
15751575
end
15761576

1577+
def test_should_automatically_save_bang_the_associated_model_if_it_sets_the_inverse_record
1578+
pirate = Pirate.new(catchphrase: "Savvy?")
1579+
ship = Ship.new(name: "Black Pearl")
1580+
ship.pirate = pirate
1581+
pirate.save!
1582+
assert_equal "Black Pearl", pirate.reload.ship.name
1583+
end
1584+
15771585
def test_should_automatically_validate_the_associated_model
15781586
@pirate.ship.name = ""
15791587
assert_predicate @pirate, :invalid?

0 commit comments

Comments
 (0)