Fix relation.create to avoid leaking scope to initialization block and callbacks

`relation.create` populates scope attributes to new record by `scoping`,
it is necessary to assign the scope attributes to the record and to find
STI subclass from the scope attributes.

But the effect of `scoping` is class global, it was caused undesired
behavior that pollute all class level querying methods in initialization
block and callbacks (`after_initialize`, `before_validation`,
`before_save`, etc), which are user provided code.

To avoid the leaking scope issue, restore the original current scope
before initialization block and callbacks are invoked.

Fixes #9894.
Fixes #17577.
Closes #31526.
This commit is contained in:
Ryuta Kamizono 2019-02-07 17:44:23 +09:00
parent 2e018361c7
commit 22360534ac
5 changed files with 45 additions and 6 deletions

@ -1,3 +1,9 @@
* Fix `relation.create` to avoid leaking scope to initialization block and callbacks.
Fixes #9894, #17577.
*Ryuta Kamizono*
* Chaining named scope is no longer leaking to class level querying methods.
Fixes #14003.

@ -67,6 +67,11 @@ def bind_attribute(name, value) # :nodoc:
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
def new(attributes = nil, &block)
current_scope = klass.current_scope(true)
block = -> record do
klass.current_scope = current_scope
yield record if block_given?
end
scoping { klass.new(attributes, &block) }
end
@ -92,7 +97,11 @@ def new(attributes = nil, &block)
# users.create(name: nil) # validation on name
# # => #<User id: nil, name: nil, ...>
def create(attributes = nil, &block)
scoping { klass.create(attributes, &block) }
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
new(attributes, &block).tap(&:save)
end
end
# Similar to #create, but calls
@ -102,7 +111,11 @@ def create(attributes = nil, &block)
# Expects arguments in the same format as
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
def create!(attributes = nil, &block)
scoping { klass.create!(attributes, &block) }
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr, &block) }
else
new(attributes, &block).tap(&:save!)
end
end
def first_or_create(attributes = nil, &block) # :nodoc:

@ -1535,7 +1535,7 @@ def test_protected_environments_are_stored_as_an_array_of_string
Bird.create!(name: "Bluejay")
ActiveRecord::Base.connection.while_preventing_writes do
assert_queries(2) { Bird.where(name: "Bluejay").explain }
assert_nothing_raised { Bird.where(name: "Bluejay").explain }
end
end

@ -1167,7 +1167,12 @@ def test_first_or_create_with_no_parameters
end
def test_first_or_create_with_block
parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" }
canary = Bird.create!(color: "yellow", name: "canary")
parrot = Bird.where(color: "green").first_or_create do |bird|
bird.name = "parrot"
assert_equal canary, Bird.find_by!(name: "canary")
end
assert_equal 1, parrot.total_count
assert_kind_of Bird, parrot
assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
@ -1209,7 +1214,12 @@ def test_first_or_create_bang_with_no_parameters
end
def test_first_or_create_bang_with_valid_block
parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" }
canary = Bird.create!(color: "yellow", name: "canary")
parrot = Bird.where(color: "green").first_or_create! do |bird|
bird.name = "parrot"
assert_equal canary, Bird.find_by!(name: "canary")
end
assert_equal 1, parrot.total_count
assert_kind_of Bird, parrot
assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
@ -1259,7 +1269,12 @@ def test_first_or_initialize_with_no_parameters
end
def test_first_or_initialize_with_block
parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" }
canary = Bird.create!(color: "yellow", name: "canary")
parrot = Bird.where(color: "green").first_or_initialize do |bird|
bird.name = "parrot"
assert_equal canary, Bird.find_by!(name: "canary")
end
assert_equal 1, parrot.total_count
assert_kind_of Bird, parrot
assert_not_predicate parrot, :persisted?
assert_predicate parrot, :valid?

@ -16,4 +16,9 @@ class Bird < ActiveRecord::Base
def cancel_save_callback_method
throw(:abort)
end
attr_accessor :total_count
after_initialize do
self.total_count = Bird.count
end
end