rails: Explicit transactions not rolling back when validation fails on nested attributes
In certain situations, when a validation fails on a model referenced in nested attributes, when the update attributes is executed inside an explicit transaction, the transaction is not rolled back, and the database is left in a state that’s different than it was before the transaction.
Example:
In the controller:
User.transaction do
user.update(email: nil, user_setting_attributes: {favourite_color: 'red'})
end
Note that user.update!
does not cause the issue.
The models:
class User < ActiveRecord::Base
has_one :user_setting
accepts_nested_attributes_for :user_setting
validates_presence_of :email
end
class UserSetting < ActiveRecord::Base
belongs_to :user
end
A complete file that shows a case that causes, and some cases that are similar but don’t cause the issue:
unless File.exist?('Gemfile')
File.write('Gemfile', <<-GEMFILE)
source 'https://rubygems.org'
gem 'activerecord', '4.1.0'
gem 'sqlite3'
GEMFILE
system 'bundle'
end
require 'bundler'
Bundler.setup(:default)
# Activate the gem you are reporting the issue against.
require 'active_record'
require 'minitest/autorun'
require 'logger'
# Ensure backward compatibility with Minitest 4
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :users do |t|
t.string :email
end
create_table :user_settings do |t|
t.integer :user_id
t.string :favourite_color
end
end
class User < ActiveRecord::Base
has_one :user_setting
accepts_nested_attributes_for :user_setting
validates_presence_of :email
end
class UserSetting < ActiveRecord::Base
belongs_to :user
end
class BugTest < Minitest::Test
def test_update_without_transaction
user = User.create!(email: 'godfrey@example.com', user_setting_attributes: {favourite_color: 'orange'})
assert_equal 'orange', user.user_setting.favourite_color
assert ! user.update(email: nil, user_setting_attributes: {favourite_color: 'red'})
user.reload
assert user.user_setting.present?
assert_equal 'orange', user.user_setting.favourite_color
end
def test_update_with_transaction
user = User.create!(email: 'godfrey@example.com', user_setting_attributes: {favourite_color: 'orange'})
assert_equal 'orange', user.user_setting.favourite_color
User.transaction do
assert ! user.update(email: nil, user_setting_attributes: {favourite_color: 'red'})
end
user.reload
assert user.user_setting.present?
assert_equal 'orange', user.user_setting.favourite_color
end
def test_update_without_transaction!
user = User.create!(email: 'godfrey@example.com', user_setting_attributes: {favourite_color: 'orange'})
assert_equal 'orange', user.user_setting.favourite_color
assert_raises(ActiveRecord::RecordInvalid) do
user.update!(email: nil, user_setting_attributes: {favourite_color: 'red'})
end
user.reload
assert user.user_setting.present?
assert_equal 'orange', user.user_setting.favourite_color
end
def test_update_with_transaction!
user = User.create!(email: 'godfrey@example.com', user_setting_attributes: {favourite_color: 'orange'})
assert_equal 'orange', user.user_setting.favourite_color
assert_raises(ActiveRecord::RecordInvalid) do
User.transaction do
assert ! user.update!(email: nil, user_setting_attributes: {favourite_color: 'red'})
end
end
user.reload
assert user.user_setting.present?
assert_equal 'orange', user.user_setting.favourite_color
end
end
If the above is in a file called repro.rb
, then:
ruby repro.rb
should produce output ending in:
4 runs, 15 assertions, 1 failures, 0 errors, 0 skips
The issues has been observed in 4.1 master and 4.0 master. I haven’t yet checked how far back it might go.
The issue was shown to my by @chancancode . I’m documenting the issue here without much further research yet because I didn’t want it to get dropped.
About this issue
- Original URL
- State: open
- Created 10 years ago
- Reactions: 3
- Comments: 20 (19 by maintainers)
Commits related to this issue
- Fixed "fake" nested transactions swallowing manual rollbacks Previously, `ActiveRecord::Rollback` are silently swallowed by all transaction blocks. The intention behind this is that these special "ex... — committed to chancancode/rails by chancancode 10 years ago
- Fixed "fake" nested transactions swallowing manual rollbacks Previously, `ActiveRecord::Rollback` are silently swallowed by all transaction blocks. The intention behind this is that these special "ex... — committed to rails/rails by chancancode 10 years ago
A blast from the past. I’ll take a look at this, perhaps this weekend.
Given this has been around for nearly a decade, perhaps it’s safe to close