Fix callbacks on has_many :through associations (#33249)

When adding a child record via a has_many :through association,
build_through_record would previously build the join record, and then
assign the child record and source_type option to it.  Because the
before_add and after_add callbacks are called as part of build, however,
this caused the callbacks to receive incomplete records, specifically
without the other end of the has_many :through association.  Collecting
all attributes before building the join record ensures the callbacks
receive the fully constructed record.
This commit is contained in:
Ryan Kerr 2019-03-30 00:35:16 -04:00
parent 4e076b03b6
commit 2e3bba3e3a
3 changed files with 29 additions and 10 deletions

@ -1,3 +1,10 @@
* Assign all attributes before calling `build` to ensure the child record is visible in
`before_add` and `after_add` callbacks for `has_many :through` associations.
Fixes #33249.
*Ryan H. Kerr*
* Add `ActiveRecord::Relation#extract_associated` for extracting associated records from a relation.
```

@ -57,21 +57,15 @@ def build_through_record(record)
@through_records[record.object_id] ||= begin
ensure_mutable
through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
if options[:source_type]
through_record.send("#{source_reflection.foreign_type}=", options[:source_type])
end
attributes = through_scope_attributes
attributes[source_reflection.name] = record
attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
through_record = through_association.build(attributes)
through_record
end
end
def options_for_through_record
[through_scope_attributes]
end
def through_scope_attributes
scope.where_values_hash(through_association.reflection.name.to_s).
except!(through_association.reflection.foreign_key,

@ -1474,6 +1474,24 @@ def self.name; "PostWithNestedHasManyThrough"; end
assert_equal [subscription2], post.subscriptions.to_a
end
def test_child_is_visible_to_join_model_in_add_association_callbacks
[:before_add, :after_add].each do |callback_name|
sentient_treasure = Class.new(Treasure) do
def self.name; "SentientTreasure"; end
has_many :pet_treasures, foreign_key: :treasure_id, callback_name => :check_pet!
has_many :pets, through: :pet_treasures
def check_pet!(added)
raise "No pet!" if added.pet.nil?
end
end
treasure = sentient_treasure.new
assert_nothing_raised { treasure.pets << pets(:mochi) }
end
end
private
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }