mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add support for bidirectional destroy dependencies
Prior to this commit if you defined a bidirectional relationship between two models with destroy dependencies on both sides, a call to `destroy` would result in an infinite callback loop. Take the following relationship. class Content < ActiveRecord::Base has_one :content_position, dependent: :destroy end class ContentPosition < ActiveRecord::Base belongs_to :content, dependent: :destroy end Calling `Content#destroy` or `ContentPosition#destroy` would result in an infinite callback loop. This commit changes the behaviour of `ActiveRecord::Callbacks#destroy` so that it guards against subsequent callbacks. Thanks to @zetter for demonstrating the issue with failing tests[1]. [1] rails#13609
This commit is contained in:
parent
090c5211ce
commit
d5bf649a53
7 changed files with 119 additions and 1 deletions
|
@ -1,3 +1,19 @@
|
|||
* Add support for bidirectional destroy dependencies.
|
||||
|
||||
Fixes #13609.
|
||||
|
||||
Example:
|
||||
|
||||
class Content < ActiveRecord::Base
|
||||
has_one :position, dependent: :destroy
|
||||
end
|
||||
|
||||
class Position < ActiveRecord::Base
|
||||
belongs_to :content, dependent: :destroy
|
||||
end
|
||||
|
||||
*Seb Jacobs*
|
||||
|
||||
* `time` columns can now affected by `time_zone_aware_attributes`. If you have
|
||||
set `config.time_zone` to a value other than `'UTC'`, they will be treated
|
||||
as in that time zone by default in Rails 5.0. If this is not the desired
|
||||
|
|
|
@ -289,7 +289,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def destroy #:nodoc:
|
||||
_run_destroy_callbacks { super }
|
||||
begin
|
||||
@_destroy_callback_already_called ||= false
|
||||
return if @_destroy_callback_already_called
|
||||
@_destroy_callback_already_called = true
|
||||
_run_destroy_callbacks { super }
|
||||
ensure
|
||||
@_destroy_callback_already_called = false
|
||||
end
|
||||
end
|
||||
|
||||
def touch(*) #:nodoc:
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
require 'cases/helper'
|
||||
require 'models/content'
|
||||
|
||||
class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase
|
||||
fixtures :content, :content_positions
|
||||
|
||||
def setup
|
||||
Content.destroyed_ids.clear
|
||||
ContentPosition.destroyed_ids.clear
|
||||
end
|
||||
|
||||
def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association
|
||||
content_position = ContentPosition.find(1)
|
||||
content = content_position.content
|
||||
assert_not_nil content
|
||||
|
||||
content_position.destroy
|
||||
|
||||
assert_equal [content_position.id], ContentPosition.destroyed_ids
|
||||
assert_equal [content.id], Content.destroyed_ids
|
||||
end
|
||||
|
||||
def test_bidirectional_dependence_when_destroying_item_with_has_one_association
|
||||
content = Content.find(1)
|
||||
content_position = content.content_position
|
||||
assert_not_nil content_position
|
||||
|
||||
content.destroy
|
||||
|
||||
assert_equal [content.id], Content.destroyed_ids
|
||||
assert_equal [content_position.id], ContentPosition.destroyed_ids
|
||||
end
|
||||
|
||||
def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time
|
||||
content = ContentWhichRequiresTwoDestroyCalls.find(1)
|
||||
|
||||
2.times { content.destroy }
|
||||
|
||||
assert_equal content.destroyed?, true
|
||||
end
|
||||
end
|
3
activerecord/test/fixtures/content.yml
vendored
Normal file
3
activerecord/test/fixtures/content.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
content:
|
||||
id: 1
|
||||
title: How to use Rails
|
3
activerecord/test/fixtures/content_positions.yml
vendored
Normal file
3
activerecord/test/fixtures/content_positions.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
content_positions:
|
||||
id: 1
|
||||
content_id: 1
|
40
activerecord/test/models/content.rb
Normal file
40
activerecord/test/models/content.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
class Content < ActiveRecord::Base
|
||||
self.table_name = 'content'
|
||||
has_one :content_position, dependent: :destroy
|
||||
|
||||
def self.destroyed_ids
|
||||
@destroyed_ids ||= []
|
||||
end
|
||||
|
||||
before_destroy do |object|
|
||||
Content.destroyed_ids << object.id
|
||||
end
|
||||
end
|
||||
|
||||
class ContentWhichRequiresTwoDestroyCalls < ActiveRecord::Base
|
||||
self.table_name = 'content'
|
||||
has_one :content_position, foreign_key: 'content_id', dependent: :destroy
|
||||
|
||||
after_initialize do
|
||||
@destroy_count = 0
|
||||
end
|
||||
|
||||
before_destroy do
|
||||
@destroy_count += 1
|
||||
if @destroy_count == 1
|
||||
throw :abort
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ContentPosition < ActiveRecord::Base
|
||||
belongs_to :content, dependent: :destroy
|
||||
|
||||
def self.destroyed_ids
|
||||
@destroyed_ids ||= []
|
||||
end
|
||||
|
||||
before_destroy do |object|
|
||||
ContentPosition.destroyed_ids << object.id
|
||||
end
|
||||
end
|
|
@ -216,6 +216,14 @@ ActiveRecord::Schema.define do
|
|||
add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
|
||||
add_index :companies, :name, name: 'company_name_index', using: :btree
|
||||
|
||||
create_table :content, force: true do |t|
|
||||
t.string :title
|
||||
end
|
||||
|
||||
create_table :content_positions, force: true do |t|
|
||||
t.integer :content_id
|
||||
end
|
||||
|
||||
create_table :vegetables, force: true do |t|
|
||||
t.string :name
|
||||
t.integer :seller_id
|
||||
|
|
Loading…
Reference in a new issue