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

Most upvoted comments

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