跳至内容 跳至搜索

Active Record 事务

事务是保护性块,其中 SQL 语句只有在它们能够作为一个原子操作全部成功时才是永久性的。典型的例子是两个账户之间的转账,只有在提款成功的情况下才能存款,反之亦然。事务强制执行数据库的完整性,并保护数据免受程序错误或数据库崩溃的影响。所以基本上,当你有一些必须一起执行或根本不执行的语句时,你应该使用事务块。

例如

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

只有在 `withdrawal` 和 `deposit` 都不会引发异常的情况下,此示例才会从 David 那里扣款并转给 Mary。异常将强制执行 `ROLLBACK`,将数据库恢复到事务开始之前的状态。但是,请注意,对象不会恢复到其事务前状态的实例数据。

单个事务中的不同 Active Record 类

尽管 transaction 类方法是在某个 Active Record 类上调用的,但事务块中的对象不必都是该类的实例。这是因为事务是每个数据库连接而不是每个模型的。

在此示例中,即使 transaction 是在 `Account` 类上调用的,`balance` 记录也会在事务中保存。

Account.transaction do
  balance.save!
  account.save!
end

transaction 方法也可以作为模型实例方法使用。例如,你也可以这样做

balance.transaction do
  balance.save!
  account.save!
end

事务不会分布在数据库连接之间

一个事务作用于单个数据库连接。如果你有多个特定于类的数据库,事务将不会保护它们之间的交互。一种解决方法是为每个要修改其模型的类启动一个事务。

Student.transaction do
  Course.transaction do
    course.enroll(student)
    student.units += course.units
  end
end

这是一个糟糕的解决方案,但完全分布式的事务超出了 Active Record 的范围。

savedestroy 会自动包装在事务中

`#save` 和 `#destroy` 都被包装在一个事务中,该事务确保你在验证或回调中所做的一切都将在其受保护的范围内进行。因此,你可以使用验证来检查事务依赖的值,或者你可以通过在回调中引发异常来回滚,包括 `after_*` 回调。

因此,对数据库的更改在操作完成之前不会在你的连接外部可见。例如,如果你尝试在 `after_save` 中更新搜索引擎的索引,索引器将看不到更新的记录。 `after_commit` 回调是唯一一个在更新提交后触发的回调。请参阅下文。

异常处理和回滚

还要记住,在事务块中引发的异常(在触发 `ROLLBACK` 后)将会传播,所以你应该准备好在你的应用程序代码中捕获它们。

一个例外是 `ActiveRecord::Rollback` 异常,当它被引发时会触发 `ROLLBACK`,但不会被事务块重新引发。任何其他异常都将被重新引发。

警告:不应该在事务块中捕获 `ActiveRecord::StatementInvalid` 异常。 `ActiveRecord::StatementInvalid` 异常表示数据库级别发生了错误,例如违反了唯一约束。在某些数据库系统(如 PostgreSQL)上,事务中的数据库错误会导致整个事务在重启之前无法使用。以下示例演示了该问题

# Suppose that we have a Number model with a unique column called 'i'.
Number.transaction do
  Number.create(i: 0)
  begin
    # This will raise a unique constraint error...
    Number.create(i: 0)
  rescue ActiveRecord::StatementInvalid
    # ...which we ignore.
  end

  # On PostgreSQL, the transaction is now unusable. The following
  # statement will cause a PostgreSQL error, even though the unique
  # constraint is no longer violated:
  Number.create(i: 1)
  # => "PG::Error: ERROR:  current transaction is aborted, commands
  #     ignored until end of transaction block"
end

如果发生 `ActiveRecord::StatementInvalid`,则应重新启动整个事务。

嵌套事务

`transaction` 调用可以嵌套。默认情况下,这使得嵌套事务块中的所有数据库语句都成为父事务的一部分。例如,以下行为可能会令人惊讶

User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

创建了“Kotori”和“Nemu”。原因是嵌套块中的 `ActiveRecord::Rollback` 异常不会发出 `ROLLBACK`。由于这些异常在事务块中被捕获,父块看不到它,并且实际事务被提交了。

为了让嵌套事务执行 `ROLLBACK`,你可以通过传递 `requires_new: true` 来要求一个真实的子事务。如果出现任何问题,数据库将回滚到子事务的开始,而不会回滚父事务。如果将其添加到前面的示例中

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

只有“Kotori”被创建。

大多数数据库不支持真正的嵌套事务。在撰写本文时,我们所知道的唯一支持真正嵌套事务的数据库是 MS-SQL。因此,Active Record 通过使用保存点来模拟嵌套事务。有关保存点的更多信息,请参阅 dev.mysql.com/doc/refman/en/savepoint.html

回调

有两种与事务提交和回滚相关的回调:`after_commit` 和 `after_rollback`。

`after_commit` 回调在事务提交后立即对事务中保存或销毁的每个记录调用。 `after_rollback` 回调在事务或保存点回滚后立即对事务中保存或销毁的每个记录调用。

这些回调对于与其他系统交互非常有用,因为你可以确保回调仅在数据库处于永久状态时执行。例如,`after_commit` 是在清除缓存时添加挂钩的好地方,因为在事务内清除缓存可能会在数据库更新之前触发缓存重新生成。

注意:回调会按过滤器进行去重。

尝试定义具有相同过滤器的多个回调将导致只运行一个回调。

例如

after_commit :do_something
after_commit :do_something # only the last one will be called

这也适用于 `after_*_commit` 回调的所有变体。

after_commit :do_something
after_create_commit :do_something
after_save_commit :do_something

建议使用 `on:` 选项来指定回调何时运行。

after_commit :do_something, on: [:create, :update]

这等同于使用 `after_create_commit` 和 `after_update_commit`,但不会去重。

注意事项

如果你使用的是 MySQL,请不要在用保存点模拟的嵌套事务块中使用数据定义语言(DDL)操作。也就是说,不要在这些块中执行如“CREATE TABLE”之类的语句。这是因为 MySQL 在执行 DDL 操作时会自动释放所有保存点。当 `transaction` 完成并尝试释放它之前创建的保存点时,由于保存点已被自动释放,将发生数据库错误。以下示例演示了该问题

Model.transaction do                           # BEGIN
  Model.transaction(requires_new: true) do     # CREATE SAVEPOINT active_record_1
    Model.lease_connection.create_table(...)   # active_record_1 now automatically released
  end                                          # RELEASE SAVEPOINT active_record_1
end                                            # ^^^^ BOOM! database error!

请注意,“TRUNCATE”也是 MySQL 的 DDL 语句!

方法
A
C
P
S
T
W

实例公共方法

after_commit(*args, &block)

在记录被创建、更新或销毁后调用此回调。

你可以通过 `:on` 选项指定回调仅由特定操作触发

after_commit :do_foo, on: :create
after_commit :do_bar, on: :update
after_commit :do_baz, on: :destroy

after_commit :do_foo_bar, on: [:create, :update]
after_commit :do_bar_baz, on: [:update, :destroy]
# File activerecord/lib/active_record/transactions.rb, line 285
def after_commit(*args, &block)
  set_options_for_callbacks!(args, prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_create_commit(*args, &block)

`after_commit :hook, on: :create` 的快捷方式。

# File activerecord/lib/active_record/transactions.rb, line 297
def after_create_commit(*args, &block)
  set_options_for_callbacks!(args, on: :create, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_destroy_commit(*args, &block)

`after_commit :hook, on: :destroy` 的快捷方式。

# File activerecord/lib/active_record/transactions.rb, line 309
def after_destroy_commit(*args, &block)
  set_options_for_callbacks!(args, on: :destroy, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_rollback(*args, &block)

在创建、更新或销毁被回滚后调用此回调。

请查看 `after_commit` 的文档以了解选项。

# File activerecord/lib/active_record/transactions.rb, line 317
def after_rollback(*args, &block)
  set_options_for_callbacks!(args, prepend_option)
  set_callback(:rollback, :after, *args, &block)
end

after_save_commit(*args, &block)

`after_commit :hook, on: [ :create, :update ]` 的快捷方式。

# File activerecord/lib/active_record/transactions.rb, line 291
def after_save_commit(*args, &block)
  set_options_for_callbacks!(args, on: [ :create, :update ], **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_update_commit(*args, &block)

`after_commit :hook, on: :update` 的快捷方式。

# File activerecord/lib/active_record/transactions.rb, line 303
def after_update_commit(*args, &block)
  set_options_for_callbacks!(args, on: :update, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

current_transaction()

返回当前事务状态的表示,它可以是顶层事务、保存点或不存在的事务。

始终返回一个对象,无论事务当前是否处于活动状态。要检查事务是否已打开,请使用 `current_transaction.open?`。

有关详细行为,请参阅 `ActiveRecord::Transaction` 文档。

# File activerecord/lib/active_record/transactions.rb, line 264
def current_transaction
  connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION
end

pool_transaction_isolation_level()

返回连接池的默认隔离级别,该级别由 `with_pool_transaction_isolation_level` 提前设置。

# File activerecord/lib/active_record/transactions.rb, line 253
def pool_transaction_isolation_level
  connection_pool.pool_transaction_isolation_level
end

set_callback(name, *filter_list, &block)

类似于 `ActiveSupport::Callbacks::ClassMethods#set_callback`,但支持 `after_commit` 和 `after_rollback` 回调上可用的选项。

# File activerecord/lib/active_record/transactions.rb, line 324
def set_callback(name, *filter_list, &block)
  options = filter_list.extract_options!
  filter_list << options

  if name.in?([:commit, :rollback]) && options[:on]
    fire_on = Array(options[:on])
    assert_valid_transaction_action(fire_on)
    options[:if] = [
      -> { transaction_include_any_action?(fire_on) },
      *options[:if]
    ]
  end


  super(name, *filter_list, &block)
end

transaction(**options, &block)

请参阅 `ConnectionAdapters::DatabaseStatements#transaction` API 文档。

# File activerecord/lib/active_record/transactions.rb, line 231
def transaction(**options, &block)
  with_connection do |connection|
    connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do
      connection.transaction(**options, &block)
    end
  end
end

with_pool_transaction_isolation_level(isolation_level, &block)

使当前池的所有事务在块内使用的隔离级别。

# File activerecord/lib/active_record/transactions.rb, line 240
def with_pool_transaction_isolation_level(isolation_level, &block)
  if current_transaction.open?
    raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
  end

  old_level = connection_pool.pool_transaction_isolation_level
  connection_pool.pool_transaction_isolation_level = isolation_level
  yield
ensure
  connection_pool.pool_transaction_isolation_level = old_level
end