跳至内容 跳至搜索

Active Record 事务

事务是保护块,其中 SQL 语句只有在所有语句都能够作为一个原子操作成功时才会永久生效。经典的例子是两个账户之间的转账,只有在取款成功的情况下才能进行存款,反之亦然。事务强制执行数据库的完整性,并保护数据免受程序错误或数据库故障的影响。因此,基本上,在您有多个必须一起执行或完全不执行的语句时,您应该使用事务块。

例如

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

此示例仅在 withdrawaldeposit 都没有引发异常的情况下才会从 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

Transactions 没有跨数据库连接分布

事务作用于单个数据库连接。如果您有多个特定于类的数据库,则事务不会保护它们之间的交互。一种解决方法是在您更改其模型的每个类上开始一个事务。

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 回调是唯一一个在更新提交后触发的回调。见下文。

Exception 处理和回滚

还要记住,在事务块中抛出的异常将被传播(在触发 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_commitafter_rollback

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

这些回调对于与其他系统交互很有用,因为您可以保证回调只在数据库处于永久状态时才执行。例如,after_commit 是一个很好的位置,用于在清除缓存时添加一个钩子,因为在事务中清除缓存可能会触发缓存重新生成,而数据库尚未更新。

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

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

例如

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_commitafter_update_commit,但不会被去重。

注意事项

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

Model.lease_connection.transaction do                           # BEGIN
  Model.lease_connection.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
                                                          # ^^^^ BOOM! database error!
end

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

方法
A
C
S
T

实例公用方法

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 266
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 278
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 290
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 298
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 272
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 284
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 245
def current_transaction
  connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION
end

set_callback(name, *filter_list, &block)

类似于 ActiveSupport::Callbacks::ClassMethods#set_callback,但支持在 after_commitafter_rollback 回调中使用的选项。

# File activerecord/lib/active_record/transactions.rb, line 305
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)

# File activerecord/lib/active_record/transactions.rb, line 232
def transaction(**options, &block)
  with_connection do |connection|
    connection.transaction(**options, &block)
  end
end