Django4.0 数据库事务-提交后
有时你需要执行与当前数据库事务相关的操作,但前提是事务成功提交。
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()
来延迟该项操作,直到事务成功后再进行操作。毕竟事务成功后你才能确保之后的操作是有意义的。