Reference

https://api.rubyonrails.org/classes/ActiveRecord/Rollback.html

http://bysxiang.com/articles/70

Cases

rails --version
Rails 6.0.0.rc1

单层事务内的普通异常


class User < ApplicationRecord
  def transaction_test
    update(attr1: nil, attr2: nil, attr3: nil, attr4: nil)
    p self

    begin
      transaction do
        update(attr1: "ATTR_ONE")
        raise StandardError, '普通异常'
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end


事务被回滚, 继续向外抛异常.


2.6.0 :004 > User.last.transaction_test
   (0.5ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 02:37:52">
   (0.1ms)  begin transaction
  User Update (0.5ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 03:35:03.740430"], ["id", 1]]
   (0.4ms)  rollback transaction
"rescue>>>"
"捕获异常: 普通异常"
"ensure>>>"
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 02:37:52">
 => "捕获异常: 普通异常" 

单层事务内的手动回滚异常


class User < ApplicationRecord
  def transaction_test
    update(attr1: nil, attr2: nil, attr3: nil, attr4: nil)
    p self

    begin
      transaction do
        update(attr1: "ATTR_ONE")
        raise ActiveRecord::Rollback, '手动回滚异常'
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

事务被回滚, ActiveRecord::Rollback 异常被吞.

2.6.0 :013 > User.last.transaction_test
   (0.1ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 02:37:52">
   (0.1ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 03:36:19.574401"], ["id", 1]]
   (0.4ms)  rollback transaction
"ensure>>>"
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 02:37:52">
 => nil 

多层事务内的普通异常

class User < ApplicationRecord
  def transaction_test
    update(attr1: nil, attr2: nil, attr3: nil, attr4: nil)
    p self

    begin
      transaction do
        update(attr1: "ATTR_ONE")
        transaction do
          update(attr2: "ATTR_TWO")
          raise StandardError, '普通异常'
        end
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

默认情况下, 会将内外两个事务合并成一个事务. 普通异常在外层被捕获, 并且继续向外抛异常.

2.6.0 :019 > User.last.transaction_test
   (0.1ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 02:37:52">
   (0.1ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 03:46:39.205584"], ["id", 1]]
  User Update (0.1ms)  UPDATE "users" SET "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr2", "ATTR_TWO"], ["updated_at", "2019-05-21 03:46:39.207658"], ["id", 1]]
   (0.3ms)  rollback transaction
"rescue>>>"
"捕获异常: 普通异常"
"ensure>>>"
  User Load (0.8ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 02:37:52">
 => "捕获异常: 普通异常"

多层事务内的手动回滚异常

class User < ApplicationRecord
  def transaction_test
    update(attr1: nil, attr2: nil, attr3: nil, attr4: nil)
    p self

    begin
      transaction do
        update(attr1: "ATTR_ONE")
        transaction do
          update(attr2: "ATTR_TWO")
          raise ActiveRecord::Rollback, '手动回滚异常'
        end
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

多个事务被合并, ActiveRecord::Rollback 在内层事务抛出, 被事务 block 阻断, 没有传到外层事务, 导致外层没有捕获到任何异常.

内层的 ActiveRecord::Rollback 实际上没有可回滚的事务了, 内层事务已经被合并到外层了.

2.6.0 :043 > User.last.transaction_test
   (0.1ms)  SELECT sqlite_version(*)
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<User id: 1, attr1: nil, attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 03:51:31">
   (0.1ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 03:53:26.407089"], ["id", 1]]
  User Update (0.1ms)  UPDATE "users" SET "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr2", "ATTR_TWO"], ["updated_at", "2019-05-21 03:53:26.408373"], ["id", 1]]
   (0.8ms)  commit transaction
"ensure>>>"
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "ATTR_ONE", attr2: "ATTR_TWO", attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 03:53:26">
 => nil 

requires_new 选项

单层事务

对于单层事务, requires_new 并没有什么不同:

class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction(requires_new: true) do
        update(attr1: "ATTR_ONE")
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end


2.6.0 :047 > reload!;User.last.transaction_test
Reloading...
   (0.1ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
   (0.0ms)  begin transaction
  User Update (0.6ms)  UPDATE "users" SET "attr1" = ?, "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "attr1"], ["attr2", nil], ["updated_at", "2019-05-21 05:27:37.863917"], ["id", 1]]
   (1.2ms)  commit transaction
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:27:37">
"---begin---"
   (0.0ms)  begin transaction
  User Update (0.2ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:27:37.867315"], ["id", 1]]
   (0.8ms)  commit transaction
"ensure>>>"
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "ATTR_ONE", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:27:37">
 => true 
2.6.0 :048 >
class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction(requires_new: true) do
        update(attr1: "ATTR_ONE")
        raise StandardError, '普通异常'
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

2.6.0 :048 > reload!;User.last.transaction_test
Reloading...
   (0.0ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
   (0.1ms)  begin transaction
  User Update (0.3ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "attr1"], ["updated_at", "2019-05-21 05:34:14.872301"], ["id", 1]]
   (0.7ms)  commit transaction
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:34:14">
"---begin---"
   (0.0ms)  begin transaction
  User Update (0.3ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:34:14.881480"], ["id", 1]]
   (0.5ms)  rollback transaction
"rescue>>>"
"捕获异常: 普通异常"
"ensure>>>"
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:34:14">
 => "捕获异常: 普通异常" 

class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction(requires_new: true) do
        update(attr1: "ATTR_ONE")
        raise ActiveRecord::Rollback, '手动回滚异常'
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

2.6.0 :049 > reload!;User.last.transaction_test
Reloading...
   (0.0ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:34:14">
"---begin---"
   (0.0ms)  begin transaction
  User Update (0.3ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:37:03.469884"], ["id", 1]]
   (0.5ms)  rollback transaction
"ensure>>>"
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:34:14">
 => nil 

多层嵌套事务

class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction(requires_new: true) do
        update(attr1: "ATTR_ONE")
        transaction do
          update(attr2: "ATTR_TWO")
        end
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

这个选项在外层事务中没有效果:

2.6.0 :065 > reload!;User.last.transaction_test
Reloading...
   (0.1ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
   (0.0ms)  begin transaction
  User Update (0.3ms)  UPDATE "users" SET "attr1" = ?, "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "attr1"], ["attr2", nil], ["updated_at", "2019-05-21 05:44:05.478124"], ["id", 1]]
   (0.8ms)  commit transaction
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:44:05">
"---begin---"
   (0.0ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:44:05.480891"], ["id", 1]]
  User Update (0.1ms)  UPDATE "users" SET "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr2", "ATTR_TWO"], ["updated_at", "2019-05-21 05:44:05.482373"], ["id", 1]]
   (0.6ms)  commit transaction
"ensure>>>"
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "ATTR_ONE", attr2: "ATTR_TWO", attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:44:05">
 => true 


class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction do
        update(attr1: "ATTR_ONE")
        transaction(requires_new: true) do
          update(attr2: "ATTR_TWO")
        end
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

在内层事务中使用 requires_new: true , 会触发数据库的 SAVEPOINT 功能, 每一层为一个子事务.

2.6.0 :068 > reload!;User.last.transaction_test
Reloading...
   (0.0ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
   (0.0ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "attr1" = ?, "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "attr1"], ["attr2", nil], ["updated_at", "2019-05-21 05:46:36.246354"], ["id", 1]]
   (1.0ms)  commit transaction
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:46:36">
"---begin---"
   (0.0ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:46:36.249353"], ["id", 1]]
   (0.0ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr2", "ATTR_TWO"], ["updated_at", "2019-05-21 05:46:36.250661"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
   (1.0ms)  commit transaction
"ensure>>>"
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "ATTR_ONE", attr2: "ATTR_TWO", attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:46:36">
 => true 
class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction do
        update(attr1: "ATTR_ONE")
        transaction(requires_new: true) do
          update(attr2: "ATTR_TWO")
          raise StandardError, '普通异常'
        end
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

2.6.0 :077 > reload!;User.last.transaction_test
Reloading...
   (0.0ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
   (0.0ms)  begin transaction
  User Update (0.3ms)  UPDATE "users" SET "attr1" = ?, "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "attr1"], ["attr2", nil], ["updated_at", "2019-05-21 05:51:06.126923"], ["id", 1]]
   (0.6ms)  commit transaction
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:51:06">
"---begin---"
   (0.1ms)  begin transaction
  User Update (0.2ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:51:06.129439"], ["id", 1]]
   (0.0ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr2", "ATTR_TWO"], ["updated_at", "2019-05-21 05:51:06.130663"], ["id", 1]]
   (0.0ms)  ROLLBACK TO SAVEPOINT active_record_1
   (0.3ms)  rollback transaction
"rescue>>>"
"捕获异常: 普通异常"
"ensure>>>"
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:51:06">
 => "捕获异常: 普通异常"
class User < ApplicationRecord
  def transaction_test
    update(attr1: 'attr1', attr2: nil, attr3: nil, attr4: nil)
    p self
    p "---begin---"
    begin
      transaction do
        update(attr1: "ATTR_ONE")
        transaction(requires_new: true) do
          update(attr2: "ATTR_TWO")
          raise ActiveRecord::Rollback, '手动回滚异常'
        end
      end
    rescue => e
      p 'rescue>>>'
      p "捕获异常: #{e}"
    ensure
      p 'ensure>>>'
      p reload
    end
  end
end

2.6.0 :081 > reload!;User.last.transaction_test
Reloading...
   (0.1ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<User id: 1, attr1: "attr1", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:51:06">
"---begin---"
   (0.1ms)  begin transaction
  User Update (0.3ms)  UPDATE "users" SET "attr1" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr1", "ATTR_ONE"], ["updated_at", "2019-05-21 05:52:08.077374"], ["id", 1]]
   (0.0ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "attr2" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["attr2", "ATTR_TWO"], ["updated_at", "2019-05-21 05:52:08.078580"], ["id", 1]]
   (0.0ms)  ROLLBACK TO SAVEPOINT active_record_1
   (0.7ms)  commit transaction
"ensure>>>"
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
#<User id: 1, attr1: "ATTR_ONE", attr2: nil, attr3: nil, attr4: nil, created_at: "2019-05-21 02:35:55", updated_at: "2019-05-21 05:52:08">
 => nil 

小结

transaction 嵌套, 默认会合并到最外层事务一起 commit .

transaction(requires_new: true) 用在内层事务, 用来触发 SAVEPOINT .

raise StandardError 会穿越 transaction 的 block 一层一层向外抛异常;

raise ActiveRecord::Rollback 会被最内层的 transaction 截获, 只对内层的 transaction 回滚有效, 对外层的 transaction 无效.