数据库事务¶
Django 提供了几种控制数据库事务管理的方法。
管理数据库事务¶
Django 的默认事务行为¶
Django 的默认行为是在自动提交模式下运行。除非事务处于活动状态,否则每个查询都会立即提交到数据库。请参阅以下详细信息。
Django 自动使用事务或保存点来保证需要多个查询的 ORM 操作的完整性,尤其是 delete() 和 update() 查询。
Django 的 TestCase
类还将每个测试包装在一个事务中以提高性能。
将事务绑定到 HTTP 请求¶
在 Web 上处理事务的一种常见方法是将每个请求包装在一个事务中。在每个要启用此行为的数据库的配置中,将 ATOMIC_REQUESTS
设置为 True
。
它的工作原理如下。在调用视图函数之前,Django 会启动一个事务。如果响应在没有问题的情况下生成,Django 会提交事务。如果视图产生异常,Django 会回滚事务。
您可以在视图代码中使用保存点执行子事务,通常使用 atomic()
上下文管理器。但是,在视图结束时,所有更改都会被提交或全部不会被提交。
警告
虽然这种事务模型的简单性很有吸引力,但当流量增加时,它也会导致效率低下。为每个视图打开一个事务会产生一些开销。对性能的影响取决于应用程序的查询模式以及数据库处理锁定的效率。
每个请求的事务和流式响应
当视图返回 StreamingHttpResponse
时,读取响应的内容通常会执行生成内容的代码。由于视图已经返回,因此此类代码在事务外部运行。
一般来说,在生成流式响应时不建议写入数据库,因为在开始发送响应后没有合理的方法来处理错误。
实际上,此功能将每个视图函数包装在下面描述的 atomic()
装饰器中。
请注意,只有视图的执行包含在事务中。中间件在事务之外运行,模板响应的渲染也是如此。
当启用 ATOMIC_REQUESTS
时,仍然可以防止视图在事务中运行。
- non_atomic_requests(using=None)[source]¶
此装饰器将否定
ATOMIC_REQUESTS
对给定视图的影响from django.db import transaction @transaction.non_atomic_requests def my_view(request): do_stuff() @transaction.non_atomic_requests(using="other") def my_other_view(request): do_stuff_on_the_other_database()
它仅在应用于视图本身时才有效。
显式控制事务¶
Django 提供了一个单一的 API 来控制数据库事务。
- atomic(using=None, savepoint=True, durable=False)[source]¶
原子性是数据库事务的定义属性。
atomic
允许我们创建一个代码块,在该代码块内保证数据库的原子性。如果代码块成功完成,则更改将提交到数据库。如果出现异常,则更改将回滚。atomic
块可以嵌套。在这种情况下,当内部块成功完成时,如果稍后在外部块中引发异常,其效果仍然可以回滚。有时需要确保
atomic
块始终是最外层的atomic
块,确保在退出块且没有错误时提交所有数据库更改。这称为持久性,可以通过设置durable=True
来实现。如果atomic
块嵌套在另一个块中,它会引发RuntimeError
。atomic
可用作 装饰器from django.db import transaction @transaction.atomic def viewfunc(request): # This code executes inside a transaction. do_stuff()
和 上下文管理器
from django.db import transaction def viewfunc(request): # This code executes in autocommit mode (Django's default). do_stuff() with transaction.atomic(): # This code executes inside a transaction. do_more_stuff()
将
atomic
包装在 try/except 块中允许自然处理完整性错误from django.db import IntegrityError, transaction @transaction.atomic def viewfunc(request): create_parent() try: with transaction.atomic(): generate_relationships() except IntegrityError: handle_exception() add_children()
在此示例中,即使
generate_relationships()
通过破坏完整性约束导致数据库错误,您也可以在add_children()
中执行查询,并且来自create_parent()
的更改仍然存在并绑定到同一个事务。请注意,在调用handle_exception()
时,generate_relationships()
中尝试执行的任何操作都将已安全回滚,因此异常处理程序也可以在必要时对数据库进行操作。避免在
atomic
内部捕获异常!退出
atomic
块时,Django 会查看它是正常退出还是出现异常以确定是否提交或回滚。如果您在atomic
块内部捕获并处理异常,您可能会隐藏 Django 发生问题的事实。这可能导致意外行为。这主要关系到
DatabaseError
及其子类(如IntegrityError
)。在发生此类错误后,事务将被破坏,Django 会在atomic
块结束时执行回滚。如果您尝试在回滚发生之前运行数据库查询,Django 将引发TransactionManagementError
。当 ORM 相关的信号处理程序引发异常时,您也可能会遇到此行为。捕获数据库错误的正确方法是在
atomic
块周围,如上所示。如有必要,为此目的添加一个额外的atomic
块。此模式还有另一个优点:它明确限定了如果发生异常将回滚哪些操作。如果您捕获由原始 SQL 查询引发的异常,Django 的行为是不确定的,并且取决于数据库。
您可能需要在回滚事务时手动恢复应用程序状态。
当发生事务回滚时,模型字段的值不会恢复。除非您手动恢复原始字段值,否则这可能导致模型状态不一致。
例如,给定一个带有
active
字段的MyModel
,这段代码片段确保了最终的if obj.active
检查在事务中将active
更新为True
失败时使用了正确的值。from django.db import DatabaseError, transaction obj = MyModel(active=False) obj.active = True try: with transaction.atomic(): obj.save() except DatabaseError: obj.active = False if obj.active: ...
这同样适用于可能保存应用程序状态的任何其他机制,例如缓存或全局变量。例如,如果代码在保存对象后主动更新缓存中的数据,建议使用 transaction.on_commit(),以便在事务实际提交后延迟缓存更改。
为了保证原子性,
atomic
禁用了某些 API。在atomic
块内尝试提交、回滚或更改数据库连接的自动提交状态将引发异常。atomic
接受一个using
参数,该参数应该是数据库的名称。如果未提供此参数,Django 将使用"default"
数据库。在底层,Django 的事务管理代码
在进入最外层的
atomic
块时打开事务;在进入内部
atomic
块时创建保存点;在退出内部块时释放或回滚到保存点;
在退出最外层块时提交或回滚事务。
可以通过将
savepoint
参数设置为False
来禁用内部块的保存点创建。如果发生异常,Django 将在退出第一个具有保存点的父块(如果存在)时执行回滚,否则将退出最外层块时执行回滚。原子性仍然由外部事务保证。仅当保存点的开销明显时才应使用此选项。它的缺点是破坏了上面描述的错误处理。当自动提交关闭时,可以使用
atomic
。它将只使用保存点,即使是最外层块也是如此。
性能注意事项
打开的事务会对您的数据库服务器造成性能开销。为了最大程度地减少此开销,请尽可能缩短事务时间。如果您在长时间运行的进程中使用 atomic()
(在 Django 的请求/响应周期之外),这一点尤其重要。
自动提交¶
为什么 Django 使用自动提交¶
在 SQL 标准中,每个 SQL 查询都会启动一个事务,除非已经有一个事务处于活动状态。然后必须显式提交或回滚此类事务。
这对于应用程序开发人员来说并不总是方便的。为了缓解这个问题,大多数数据库都提供了一种自动提交模式。当自动提交打开且没有事务处于活动状态时,每个 SQL 查询都会包装在它自己的事务中。换句话说,每个这样的查询不仅会启动一个事务,而且还会根据查询是否成功自动提交或回滚事务。
PEP 249,Python 数据库 API 规范 v2.0,要求最初关闭自动提交。Django 覆盖了此默认设置并打开了自动提交。
要避免这种情况,您可以 停用事务管理,但并不推荐。
停用事务管理¶
您可以通过在其配置中将 AUTOCOMMIT
设置为 False
来完全禁用给定数据库的 Django 事务管理。如果您这样做,Django 不会启用自动提交,也不会执行任何提交操作。您将获得底层数据库库的常规行为。
这要求您显式提交每个事务,即使是 Django 或第三方库启动的事务也是如此。因此,这最好用于您希望运行自己的事务控制中间件或执行一些非常奇怪的操作的情况。
提交后执行操作¶
有时您需要执行与当前数据库事务相关的操作,但仅当事务成功提交时才执行。示例可能包括后台任务、电子邮件通知或缓存失效。
on_commit()
允许您注册回调,这些回调将在打开的事务成功提交后执行。
将函数或任何可调用对象传递给 on_commit()
from django.db import transaction
def send_welcome_email(): ...
transaction.on_commit(send_welcome_email)
回调不会传递任何参数,但您可以使用 functools.partial()
将它们绑定。
from functools import partial
for user in users:
transaction.on_commit(partial(send_invite_email, user=user))
在打开的事务成功提交后调用回调。如果事务被回滚(通常是在 atomic()
块中引发未处理的异常时),则回调将被丢弃,并且永远不会被调用。
如果您在没有打开的事务时调用 on_commit()
,则回调将立即执行。
注册可能失败的回调有时很有用。传递 robust=True
允许即使当前回调引发异常也能执行后续回调。所有从 Python 的 Exception
类派生的错误都会被捕获并记录到 django.db.backends.base
记录器中。
您可以使用 TestCase.captureOnCommitCallbacks()
测试使用 on_commit()
注册的回调。
保存点¶
保存点(即嵌套的 atomic()
块)会得到正确的处理。也就是说,在保存点(在嵌套的 atomic()
块中)之后注册的 on_commit()
可调用对象将在外部事务提交后被调用,但如果在事务期间发生了到该保存点或任何先前保存点的回滚,则不会被调用。
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()
执行顺序¶
给定事务的提交后函数按注册顺序执行。
异常处理¶
如果在给定事务中使用 robust=False
注册的一个提交后函数引发了未捕获的异常,则同一事务中以后注册的函数将不会运行。这与您自己按顺序执行函数(不使用 on_commit()
)时的行为相同。
执行时间¶
您的回调在成功提交之后执行,因此回调中的错误不会导致事务回滚。它们根据事务的成功情况有条件地执行,但它们不是事务的一部分。对于预期的用例(邮件通知、后台任务等),这应该没问题。如果不是(如果您的后续操作非常关键,以至于它的失败意味着事务本身的失败),那么您不希望使用 on_commit()
钩子。相反,您可能希望使用 两阶段提交,例如 psycopg 两阶段提交协议支持 和 Python DB-API 规范中的可选两阶段提交扩展。
在提交后恢复连接上的自动提交之前,回调不会运行(因为否则回调中执行的任何查询都会打开一个隐式事务,从而阻止连接恢复到自动提交模式)。
在自动提交模式下且在 atomic()
块之外,该函数将立即运行,而不是在提交时运行。
仅当使用自动提交模式和atomic()
(或ATOMIC_REQUESTS
)事务 API 时,提交函数才有效。当自动提交被禁用且您不在原子块中时调用on_commit()
将导致错误。
在测试中使用¶
Django 的TestCase
类将每个测试包装在一个事务中,并在每个测试后回滚该事务,以提供测试隔离。这意味着永远不会真正提交任何事务,因此您的on_commit()
回调永远不会运行。
您可以使用TestCase.captureOnCommitCallbacks()
克服此限制。这会将您的on_commit()
回调捕获到一个列表中,允许您对其进行断言,或通过调用它们来模拟事务提交。
克服此限制的另一种方法是使用TransactionTestCase
而不是TestCase
。这意味着您的事务将被提交,并且回调将运行。但是TransactionTestCase
会在测试之间刷新数据库,这比TestCase
的隔离速度慢得多。
为什么没有回滚钩子?¶
与提交钩子相比,回滚钩子更难以可靠地实现,因为各种情况都可能导致隐式回滚。
例如,如果您的数据库连接由于您的进程被终止而无法正常关闭而断开,则您的回滚钩子将永远不会运行。
但是有一个解决方案:不要在原子块(事务)期间执行某些操作,然后在事务失败时撤消它,而是使用on_commit()
将执行延迟到事务成功之后。撤消您从未执行过的操作要容易得多!
底层 API¶
自动提交¶
Django 在django.db.transaction
模块中提供了一个 API 来管理每个数据库连接的自动提交状态。
这些函数接受一个using
参数,该参数应该是数据库的名称。如果未提供,Django 将使用"default"
数据库。
自动提交最初是开启的。如果您将其关闭,则您有责任恢复它。
一旦您关闭自动提交,您将获得数据库适配器的默认行为,而 Django 不会帮助您。虽然该行为在PEP 249中指定,但适配器的实现并不总是彼此一致。仔细查看您正在使用的适配器的文档。
在重新打开自动提交之前,您必须确保没有活动的事务,通常是通过发出commit()
或rollback()
来实现。
当atomic()
块处于活动状态时,Django 将拒绝关闭自动提交,因为这会破坏原子性。
事务¶
事务是一组原子数据库查询。即使您的程序崩溃,数据库也保证所有更改要么全部应用,要么都不应用。
Django 没有提供开始事务的 API。开始事务的预期方法是使用set_autocommit()
禁用自动提交。
进入事务后,您可以选择使用commit()
应用到目前为止执行的更改,或者使用rollback()
取消它们。这些函数在django.db.transaction
中定义。
这些函数接受一个using
参数,该参数应该是数据库的名称。如果未提供,Django 将使用"default"
数据库。
当atomic()
块处于活动状态时,Django 将拒绝提交或回滚,因为这会破坏原子性。
保存点¶
保存点是事务中的一个标记,它使您能够回滚事务的一部分,而不是整个事务。保存点可用于 SQLite、PostgreSQL、Oracle 和 MySQL(当使用 InnoDB 存储引擎时)后端。其他后端提供了保存点函数,但它们是空操作——它们实际上什么也不做。
如果您使用自动提交(Django 的默认行为),则保存点不是特别有用。但是,一旦您使用atomic()
打开事务,您就会构建一系列等待提交或回滚的数据库操作。如果发出回滚,则整个事务将回滚。保存点提供执行细粒度回滚的能力,而不是transaction.rollback()
将执行的完整回滚。
当atomic()
装饰器嵌套时,它会创建一个保存点以允许部分提交或回滚。强烈建议您使用atomic()
而不是下面描述的函数,但它们仍然是公共 API 的一部分,并且没有计划弃用它们。
这些函数中的每一个都接受一个using
参数,该参数应该是行为适用的数据库的名称。如果没有提供using
参数,则使用"default"
数据库。
django.db.transaction
中的三个函数控制保存点
如果数据库不支持保存点或处于自动提交模式,则这些函数不会执行任何操作。
此外,还有一个实用程序函数
以下示例演示了保存点的用法
from django.db import transaction
# open a transaction
@transaction.atomic
def viewfunc(request):
a.save()
# transaction now contains a.save()
sid = transaction.savepoint()
b.save()
# transaction now contains a.save() and b.save()
if want_to_keep_b:
transaction.savepoint_commit(sid)
# open transaction still contains a.save() and b.save()
else:
transaction.savepoint_rollback(sid)
# open transaction now contains only a.save()
保存点可用于通过执行部分回滚来从数据库错误中恢复。如果在 atomic()
块内执行此操作,则整个块仍将回滚,因为它不知道您已在较低级别处理了这种情况!为了防止这种情况,您可以使用以下函数控制回滚行为。
将回滚标志设置为 True
会在退出最内层原子块时强制回滚。这可能有助于在不引发异常的情况下触发回滚。
将其设置为 False
会阻止此类回滚。在执行此操作之前,请确保已将事务回滚到当前原子块内的已知良好保存点!否则,您将破坏原子性,并可能导致数据损坏。
数据库特定说明¶
SQLite 中的保存点¶
虽然 SQLite 支持保存点,但 sqlite3
模块设计中的一个缺陷使得它们几乎无法使用。
启用自动提交时,保存点没有意义。禁用时,sqlite3
在保存点语句之前隐式提交。 (实际上,它在除 SELECT
、INSERT
、UPDATE
、DELETE
和 REPLACE
之外的任何语句之前都会提交。)此错误有两个后果
MySQL 中的事务¶
如果您使用的是 MySQL,则您的表可能支持也可能不支持事务;这取决于您的 MySQL 版本和您使用的表类型。(“表类型”是指“InnoDB”或“MyISAM”之类的东西。)MySQL 事务的特殊性不在本文讨论范围之内,但 MySQL 站点上有 有关 MySQL 事务的信息。
如果您的 MySQL 设置不支持事务,则 Django 将始终在自动提交模式下运行:语句将在调用后立即执行并提交。如果您的 MySQL 设置支持事务,则 Django 将按照本文档中所述处理事务。
处理 PostgreSQL 事务中的异常¶
注意
本节仅在您实现自己的事务管理时才相关。此问题在 Django 的默认模式下不会发生,并且 atomic()
会自动处理它。
在事务中,当对 PostgreSQL 游标的调用引发异常(通常为 IntegrityError
)时,同一事务中的所有后续 SQL 都将失败,并显示错误“当前事务已中止,查询在事务块结束前被忽略”。虽然 save()
的基本用法不太可能在 PostgreSQL 中引发异常,但存在可能引发异常的更高级的用法模式,例如保存具有唯一字段的对象、使用 force_insert
/force_update
标志保存或调用自定义 SQL。
有几种方法可以从这种错误中恢复。
事务回滚¶
第一个选项是回滚整个事务。例如
a.save() # Succeeds, but may be undone by transaction rollback
try:
b.save() # Could throw exception
except IntegrityError:
transaction.rollback()
c.save() # Succeeds, but a.save() may have been undone
调用 transaction.rollback()
会回滚整个事务。任何未提交的数据库操作都将丢失。在此示例中,a.save()
所做的更改将丢失,即使该操作本身没有引发错误。
保存点回滚¶
您可以使用 保存点 来控制回滚的范围。在执行可能失败的数据库操作之前,您可以设置或更新保存点;这样,如果操作失败,您可以回滚单个有问题的操作,而不是整个事务。例如
a.save() # Succeeds, and never undone by savepoint rollback
sid = transaction.savepoint()
try:
b.save() # Could throw exception
transaction.savepoint_commit(sid)
except IntegrityError:
transaction.savepoint_rollback(sid)
c.save() # Succeeds, and a.save() is never undone
在此示例中,如果 b.save()
引发异常,则 a.save()
不会被撤消。