活动记录事务
事务是保护性块,其中 SQL 语句仅在它们都能作为单个原子操作成功时才永久存在。经典示例是两个帐户之间的转账,其中只有在取款成功时才能存款,反之亦然。事务强制执行数据库的完整性,并保护数据免受程序错误或数据库崩溃的影响。因此,基本上,只要你有一些必须一起执行或根本不执行的语句,就应该使用事务块。
例如
ActiveRecord::Base.transaction do
david.withdrawal(100)
mary.deposit(100)
end
此示例仅在withdrawal
和deposit
都不引发异常的情况下从 David 取款并将其交给 Mary。异常将强制进行 ROLLBACK,使数据库返回到事务开始前的状态。但是,请注意,对象不会将其实例数据返回到事务前状态。
单个事务中的不同活动记录类
虽然transaction
类方法是在某个活动记录类上调用的,但事务块中的对象不必都是该类的实例。这是因为事务是按数据库连接进行的,而不是按模型进行的。
在此示例中,即使在Account
类上调用transaction
,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 的范围。
save
和 destroy
会自动封装在一个事务中
#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_commit
和 after_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]
after_create_commit(*args, &block) 链接
after_commit :hook, on: :create
的快捷方式。
after_destroy_commit(*args, &block) 链接
after_commit :hook, on: :destroy
的快捷方式。
after_rollback(*args, &block) 链接
在回滚创建、更新或销毁后调用此回调。
有关选项,请查看 after_commit
的文档。
after_save_commit(*args, &block) 链接
after_commit :hook, on: [ :create, :update ]
的快捷方式。
after_update_commit(*args, &block) 链接
after_commit :hook, on: :update
的快捷方式。
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/transactions.rb, line 211 def transaction(**options, &block) connection.transaction(**options, &block) end