Skip to content

Commit faf07d1

Browse files
committed
Merge pull request rails#28155 from lcreid/belongs_to
Fix "autosave: true" on belongs_to of join model causes invalid records to be saved
2 parents 7cb3e8b + 332e760 commit faf07d1

File tree

7 files changed

+72
-2
lines changed

7 files changed

+72
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
* Fix circular `autosave: true` causes invalid records to be saved.
2+
3+
Prior to the fix, when there was a circular series of `autosave: true`
4+
associations, the callback for a `has_many` association was run while
5+
another instance of the same callback on the same association hadn't
6+
finished running. When control returned to the first instance of the
7+
callback, the instance variable had changed, and subsequent associated
8+
records weren't saved correctly. Specifically, the ID field for the
9+
`belongs_to` corresponding to the `has_many` was `nil`.
10+
11+
Fixes #28080.
12+
13+
*Larry Reid*
14+
115
* Raise `ArgumentError` for invalid `:limit` and `:precision` like as other options.
216

317
Before:

activerecord/lib/active_record/autosave_association.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,14 @@ def save_collection_association(reflection)
382382
if association = association_instance_get(reflection.name)
383383
autosave = reflection.options[:autosave]
384384

385+
# By saving the instance variable in a local variable,
386+
# we make the whole callback re-entrant.
387+
new_record_before_save = @new_record_before_save
388+
385389
# reconstruct the scope now that we know the owner's id
386390
association.reset_scope
387391

388-
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
392+
if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave)
389393
if autosave
390394
records_to_destroy = records.select(&:marked_for_destruction?)
391395
records_to_destroy.each { |record| association.destroy(record) }
@@ -397,7 +401,7 @@ def save_collection_association(reflection)
397401

398402
saved = true
399403

400-
if autosave != false && (@new_record_before_save || record.new_record?)
404+
if autosave != false && (new_record_before_save || record.new_record?)
401405
if autosave
402406
saved = association.insert_record(record, false)
403407
elsif !reflection.nested?

activerecord/test/cases/associations/has_many_through_associations_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
require "models/user"
3434
require "models/family"
3535
require "models/family_tree"
36+
require "models/section"
37+
require "models/seminar"
38+
require "models/session"
3639

3740
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
3841
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
@@ -1492,6 +1495,19 @@ def check_pet!(added)
14921495
end
14931496
end
14941497

1498+
def test_circular_autosave_association_correctly_saves_multiple_records
1499+
cs180 = Seminar.new(name: "CS180")
1500+
fall = Session.new(name: "Fall")
1501+
sections = [
1502+
cs180.sections.build(short_name: "A"),
1503+
cs180.sections.build(short_name: "B"),
1504+
]
1505+
fall.sections << sections
1506+
fall.save!
1507+
fall.reload
1508+
assert_equal sections, fall.sections.sort_by(&:id)
1509+
end
1510+
14951511
private
14961512
def make_model(name)
14971513
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }

activerecord/test/models/section.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
class Section < ActiveRecord::Base
4+
belongs_to :session, inverse_of: :sections, autosave: true
5+
belongs_to :seminar, inverse_of: :sections, autosave: true
6+
end

activerecord/test/models/seminar.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
class Seminar < ActiveRecord::Base
4+
has_many :sections, inverse_of: :seminar, autosave: true, dependent: :destroy
5+
has_many :sessions, through: :sections
6+
end

activerecord/test/models/session.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
class Session < ActiveRecord::Base
4+
has_many :sections, inverse_of: :session, autosave: true, dependent: :destroy
5+
has_many :seminars, through: :sections
6+
end

activerecord/test/schema/schema.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,24 @@
792792
t.integer :lock_version, default: 0
793793
end
794794

795+
disable_referential_integrity do
796+
create_table :seminars, force: :cascade do |t|
797+
t.string :name
798+
end
799+
800+
create_table :sessions, force: :cascade do |t|
801+
t.date :start_date
802+
t.date :end_date
803+
t.string :name
804+
end
805+
806+
create_table :sections, force: :cascade do |t|
807+
t.string :short_name
808+
t.belongs_to :session, foreign_key: true
809+
t.belongs_to :seminar, foreign_key: true
810+
end
811+
end
812+
795813
create_table :shape_expressions, force: true do |t|
796814
t.string :paint_type
797815
t.integer :paint_id

0 commit comments

Comments
 (0)