aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/collection_association.rb
diff options
context:
space:
mode:
authorRyuta Kamizono <kamipo@gmail.com>2017-04-02 08:24:23 +0900
committerRyuta Kamizono <kamipo@gmail.com>2017-04-21 07:20:52 +0900
commitc0038f7c362fa0c92fc9e1ea3bdb2706f42386c6 (patch)
tree0713cb06b1f2be74776d0b52c88ac89b2e3acbf8 /activerecord/lib/active_record/associations/collection_association.rb
parent972df059bbedfe60d29caa8a561f5aff76883e63 (diff)
downloadrails-c0038f7c362fa0c92fc9e1ea3bdb2706f42386c6.tar.gz
rails-c0038f7c362fa0c92fc9e1ea3bdb2706f42386c6.tar.bz2
rails-c0038f7c362fa0c92fc9e1ea3bdb2706f42386c6.zip
Prevent double firing the before save callback of new object when the parent association saved in the callback
Related #18155, #26661, 268a5bb, #27434, #27442, and #28599. Originally #18155 was introduced for preventing double insertion caused by the after save callback. But it was caused the before save issue (#26661). 268a5bb fixed #26661, but it was caused the performance regression (#27434). #27442 added new record to `target` before calling callbacks for fixing #27434. But it was caused double firing before save callback (#28599). We cannot add new object to `target` before saving the object. This is improving #18155 to only track callbacks after `save`. Fixes #28599.
Diffstat (limited to 'activerecord/lib/active_record/associations/collection_association.rb')
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb67
1 files changed, 32 insertions, 35 deletions
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 77282e6463..62c944fce3 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -280,35 +280,6 @@ module ActiveRecord
replace_on_target(record, index, skip_callbacks, &block)
end
- def replace_on_target(record, index, skip_callbacks)
- callback(:before_add, record) unless skip_callbacks
-
- begin
- if index
- record_was = target[index]
- target[index] = record
- else
- target << record
- end
-
- set_inverse_instance(record)
-
- yield(record) if block_given?
- rescue
- if index
- target[index] = record_was
- else
- target.delete(record)
- end
-
- raise
- end
-
- callback(:after_add, record) unless skip_callbacks
-
- record
- end
-
def scope
scope = super
scope.none! if null_scope?
@@ -385,15 +356,19 @@ module ActiveRecord
transaction do
add_to_target(build_record(attributes)) do |record|
yield(record) if block_given?
- insert_record(record, true, raise)
+ insert_record(record, true, raise) { @_was_loaded = loaded? }
end
end
end
end
# Do the relevant stuff to insert the given record into the association collection.
- def insert_record(record, validate = true, raise = false)
- raise NotImplementedError
+ def insert_record(record, validate = true, raise = false, &block)
+ if raise
+ record.save!(validate: validate, &block)
+ else
+ record.save(validate: validate, &block)
+ end
end
def create_scope
@@ -448,19 +423,41 @@ module ActiveRecord
end
end
- def concat_records(records, should_raise = false)
+ def concat_records(records, raise = false)
result = true
records.each do |record|
raise_on_type_mismatch!(record)
- add_to_target(record) do |rec|
- result &&= insert_record(rec, true, should_raise) unless owner.new_record?
+ add_to_target(record) do
+ result &&= insert_record(record, true, raise) { @_was_loaded = loaded? } unless owner.new_record?
end
end
result && records
end
+ def replace_on_target(record, index, skip_callbacks)
+ callback(:before_add, record) unless skip_callbacks
+
+ set_inverse_instance(record)
+
+ @_was_loaded = true
+
+ yield(record) if block_given?
+
+ if index
+ target[index] = record
+ elsif @_was_loaded || !loaded?
+ target << record
+ end
+
+ callback(:after_add, record) unless skip_callbacks
+
+ record
+ ensure
+ @_was_loaded = nil
+ end
+
def callback(method, record)
callbacks_for(method).each do |callback|
callback.call(method, owner, record)