跳至内容 跳至搜索

活动记录事务

事务是保护性块,其中 SQL 语句仅在它们都能作为单个原子操作成功时才永久存在。经典示例是两个帐户之间的转账,其中只有在取款成功时才能存款,反之亦然。事务强制执行数据库的完整性,并保护数据免受程序错误或数据库崩溃的影响。因此,基本上,只要你有一些必须一起执行或根本不执行的语句,就应该使用事务块。

例如

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

此示例仅在withdrawaldeposit都不引发异常的情况下从 David 取款并将其交给 Mary。异常将强制进行 ROLLBACK,使数据库返回到事务开始前的状态。但是,请注意,对象不会将其实例数据返回到事务前状态。

单个事务中的不同活动记录类

虽然transaction类方法是在某个活动记录类上调用的,但事务块中的对象不必都是该类的实例。这是因为事务是按数据库连接进行的,而不是按模型进行的。

在此示例中,即使在Account类上调用transactionbalance记录也会以事务方式保存

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。由于这些异常是在事务块中捕获的,所以父块看不到它,并且实际事务被提交。

为了对嵌套事务进行回滚,您可以通过传递 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 是放置一个挂钩来清除缓存的好地方,因为在事务中清除缓存可能会在数据库更新之前触发缓存重新生成。

注意事项

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

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

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

方法
A
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 232
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 244
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 256
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 264
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 238
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 250
def after_update_commit(*args, &block)
  set_options_for_callbacks!(args, on: :update, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

transaction(**options, &block)

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