Django4 中文入门教程 Django4.0 数据库事务-提交后

2024-02-25 开发教程 Django4 中文入门教程 匿名 3

有时你需要执行与当前数据库事务相关的操作,但前提是事务成功提交。

Django 提供了 on_commit() 函数来注册在事务成功提交后应该执行的回调函数:

on_commit(func, using=None)

将任意函数(无参数)传递给 ​on_commit()​:

from django.db import transaction
def do_something():
pass # send a mail, invalidate a cache, fire off a Celery task, etc.
transaction.on_commit(do_something)

你也可以使用 ​lambda​包装函数:

transaction.on_commit(lambda: some_celery_task.delay('arg1'))

传入的函数将在成功提交调用“​on_commit()​”的假设数据库写操作后立即被调用。
无任何活动事务时调用 ​on_commit()​ ,则回调函数会立即执行。
如果假设的数据库写入被回滚(尤其是在 ​atomic()​ 块里引发了一个未处理异常),函数将被丢弃且永远不会被调用。

保存点

正确处理保存点(即嵌套了 ​atomic()​ 块)。也就是说,注册在保存点后的 ​on_commit()​ 的调用(嵌套在 ​atomic()​ 块)将在外部事务被提交之后调用,但如果在事务期间回滚到保存点或任何之前的保存点之前,则不会调用:

with transaction.atomic():  # Outer atomic, start a new transaction
transaction.on_commit(foo)
with transaction.atomic(): # Inner atomic block, create a savepoint
transaction.on_commit(bar)
# foo() and then bar() will be called when leaving the outermost block

另一方面,当保存点回滚时(因引发异常),内部调用不会被调用:

with transaction.atomic():  # Outer atomic, start a new transaction
transaction.on_commit(foo)
try:
with transaction.atomic(): # Inner atomic block, create a savepoint
transaction.on_commit(bar)
raise SomeError() # Raising an exception - abort the savepoint
except SomeError:
pass
# foo() will be called, but not bar()

执行顺序

事务提交后的的回调函数执行顺序与当初注册时的顺序一致。

异常处理

如果一个带有给定事务的 ​on-commit​ 函数引发了未捕获的异常,那么同一个事务里的后续注册函数不会被运行。这与你在没有 ​on_commit()​ 的情况下顺序执行函数的行为是一样的。

执行时间

你的回调会在成功提交之后执行,因此回调里的错误引发事务回滚。它们在事务成功时有条件的执行,但它们不是事务的一部分。对于有预期的用例(邮件提醒,Celery 任务等),这样应该没啥问题。如果它不是这样的用例(如果你的后续操作很关键,以至于它的错误意味着事务失败),那么你可能不需要使用 ​on_commit()​ 钩子。相反,你可能需要两阶段提交——比如两阶段提交协议支持( psycopg Two-Phase Commit protocol support)和在 Python DB-API 里说明的可选两阶段提交扩展( optional Two-Phase Commit Extensions in the Python DB-API specification) 。
直到在提交后的连接上恢复自动提交,调用才会运行。(因为否则在回调中完成的任何查询都会打开一个隐式事务,防止连接返回自动提交模式)
当在自动提交模式并且在 ​atomic()​ 块外时,函数会立即自动运行,而不会提交。
on-commit​ 函数仅适用于自动提交模式( ​autocommit mode​ ),并且 ​atomic()​ (或 ​ATOMIC_REQUESTS​ )事务API。当禁用自动提交并且当前不在原子块中时,调用 ​on_commit()​ 将导致错误。

在测试中使用

Django 的 ​TestCase类将每个测试包装在一个事务中,并在每次测试后回滚该事务,以提供测试隔离。 这意味着实际上没有任何事务被提交,因此您的 ​on_commit()​ 回调将永远不会运行。

您可以通过使用 ​TestCase.captureOnCommitCallbacks()​ 来克服这个限制。 这会在列表中捕获您的 ​on_commit()​ 回调,允许您对它们进行断言,或通过调用它们来模拟事务提交。

克服限制的另一种方法是使用 ​TransactionTestCase​ 而不是 ​TestCase​。 这意味着您的事务已提交,并且回调将运行。 但是 ​TransactionTestCase在测试之间刷新数据库,这比 ​TestCase的隔离要慢得多。

为什么没有事务回滚钩子

事务回滚钩子相比事务提交钩子更难实现,因为各种各样的情况都可能造成隐式回滚。
比如,如果数据库连接被删除,因为进程被杀而没有机会正常关闭,回滚钩子将不会运行。
解决方法是:与其在执行事务时(原子操作)进行某项操作,当事务执行失败后再取消这项操作,不如使用 ​on_commit()​ 来延迟该项操作,直到事务成功后再进行操作。毕竟事务成功后你才能确保之后的操作是有意义的。