数据库事务

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)

此装饰器将抵消 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)

原子性是数据库事务的定义属性。 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,此代码片段确保如果在事务中将active更新为True失败,则最后的if obj.active检查使用正确的值

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。它只使用保存点,即使是最外层的块也是如此。

性能注意事项

打开事务会对您的数据库服务器造成性能开销。为了最大程度地减少这种开销,请尽可能缩短事务的持续时间。如果您在 Django 的请求/响应循环之外,在长时间运行的进程中使用 atomic(),这一点尤其重要。

自动提交

为什么 Django 使用自动提交

在 SQL 标准中,每个 SQL 查询都会启动一个事务,除非已经存在一个活动的事务。然后必须显式地提交或回滚这些事务。

这对应用程序开发人员来说并不总是方便。为了解决这个问题,大多数数据库都提供了自动提交模式。当自动提交打开且没有活动的事务时,每个 SQL 查询都会被包装在它自己的事务中。换句话说,不仅每个这样的查询都会启动一个事务,而且该事务也会根据查询是否成功自动提交或回滚。

PEP 249,Python 数据库 API 规范 v2.0,要求最初关闭自动提交。Django 覆盖了此默认设置并打开了自动提交。

为了避免这种情况,您可以 停用事务管理,但不建议这样做。

停用事务管理

您可以通过在数据库配置中将 AUTOCOMMIT 设置为 False,完全禁用 Django 对给定数据库的事务管理。如果您这样做,Django 不会启用自动提交,也不会执行任何提交操作。您将获得底层数据库库的常规行为。

这要求您显式地提交每个事务,即使是 Django 或第三方库启动的事务。因此,这最适合您想要运行自己的事务控制中间件或做一些非常奇怪的事情的情况。

在提交后执行操作

有时您需要执行与当前数据库事务相关的操作,但仅当事务成功提交时才执行。例如,可能包括后台任务、电子邮件通知或缓存失效。

on_commit() 允许您注册在打开的事务成功提交后执行的回调函数。

on_commit(func, using=None, robust=False)

将函数或任何可调用对象传递给 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() 注册的回调函数。

Django 4.2 中的变更

添加了 robust 参数。

保存点

保存点(即嵌套的 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()

执行顺序

给定事务的 on-commit 函数按注册顺序执行。

异常处理

如果在给定事务中使用 robust=False 注册的一个 on-commit 函数引发未捕获的异常,则该事务中注册的任何后续函数都不会运行。这与您自己按顺序执行函数(不使用 on_commit())的行为相同。

Django 4.2 中的变更

添加了 robust 参数。

执行时间

您的回调将在成功提交之后执行,因此回调中的失败不会导致事务回滚。它们根据事务的成功情况有条件地执行,但它们不是事务的一部分。对于预期的用例(邮件通知、后台任务等),这应该没问题。如果不是(如果您的后续操作非常关键,以至于它的失败意味着事务本身的失败),那么您不想使用 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

警告

如果可能,始终优先使用 atomic()。它考虑了每个数据库的特性,并防止无效操作。

只有在您实现自己的事务管理时,低级 API 才有用。

自动提交

Django 在 django.db.transaction 模块中提供了一个 API 来管理每个数据库连接的自动提交状态。

get_autocommit(using=None)
set_autocommit(autocommit, using=None)

这些函数接受一个 using 参数,该参数应该是数据库的名称。如果未提供,Django 将使用 "default" 数据库。

自动提交默认开启。如果关闭它,您有责任恢复它。

关闭自动提交后,您将获得数据库适配器的默认行为,Django 不会帮助您。虽然这种行为在 PEP 249 中有规定,但适配器的实现并不总是彼此一致。请仔细查看您正在使用的适配器的文档。

您必须确保没有活动的事务,通常通过发出 commit()rollback(),然后再重新开启自动提交。

atomic() 块处于活动状态时,Django 会拒绝关闭自动提交,因为这会破坏原子性。

事务

事务是一组原子性的数据库查询。即使您的程序崩溃,数据库也能保证所有更改要么全部应用,要么全部不应用。

Django 没有提供启动事务的 API。启动事务的预期方法是使用 set_autocommit() 禁用自动提交。

进入事务后,您可以选择使用 commit() 应用到目前为止执行的更改,或者使用 rollback() 取消更改。这些函数在 django.db.transaction 中定义。

commit(using=None)
rollback(using=None)

这些函数接受一个 using 参数,该参数应该是数据库的名称。如果未提供,Django 将使用 "default" 数据库。

atomic() 块处于活动状态时,Django 会拒绝提交或回滚,因为这会破坏原子性。

保存点

保存点是事务中的一个标记,它使您能够回滚事务的一部分,而不是整个事务。保存点在 SQLite、PostgreSQL、Oracle 和 MySQL(使用 InnoDB 存储引擎时)后端可用。其他后端提供保存点函数,但它们是空操作 - 它们实际上什么也不做。

如果您使用的是自动提交(Django 的默认行为),保存点并不特别有用。但是,一旦您使用 atomic() 打开事务,您就会构建一系列等待提交或回滚的数据库操作。如果您发出回滚命令,整个事务将被回滚。保存点提供了执行细粒度回滚的能力,而不是 transaction.rollback() 将执行的完全回滚。

atomic() 装饰器嵌套时,它会创建一个保存点以允许部分提交或回滚。强烈建议您使用 atomic() 而不是下面描述的函数,但它们仍然是公共 API 的一部分,并且没有计划弃用它们。

这些函数中的每一个都接受一个 using 参数,该参数应该是行为适用的数据库的名称。如果未提供 using 参数,则使用 "default" 数据库。

保存点由 django.db.transaction 中的三个函数控制。

savepoint(using=None)

创建一个新的保存点。这将标记事务中已知处于“良好”状态的点。返回保存点 ID (sid)。

savepoint_commit(sid, using=None)

释放保存点 sid。自创建保存点以来执行的更改将成为事务的一部分。

savepoint_rollback(sid, using=None)

将事务回滚到保存点 sid

如果数据库不支持保存点或处于自动提交模式,则这些函数将不执行任何操作。

此外,还有一个实用程序函数

clean_savepoints(using=None)

重置用于生成唯一保存点 ID 的计数器。

以下示例演示了保存点的使用

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() 块内执行此操作,整个块仍将回滚,因为它不知道您已在较低级别处理了这种情况!为了防止这种情况,您可以使用以下函数控制回滚行为。

get_rollback(using=None)
set_rollback(rollback, using=None)

将回滚标志设置为 True 会在退出最内层的原子块时强制回滚。这可能有助于在不引发异常的情况下触发回滚。

将其设置为 False 会阻止这种回滚。在这样做之前,请确保您已在当前原子块中将事务回滚到已知的良好保存点!否则,您将破坏原子性,可能会导致数据损坏。

数据库特定说明

SQLite 中的保存点

虽然 SQLite 支持保存点,但 sqlite3 模块设计中的缺陷使其几乎无法使用。

当启用自动提交时,保存点没有意义。当它被禁用时,sqlite3 会在保存点语句之前隐式提交。(实际上,它会在除 SELECTINSERTUPDATEDELETEREPLACE 之外的任何语句之前提交。)此错误有两个后果

  • 保存点的低级 API 只能在事务内使用,即在 atomic() 块内。
  • 当关闭自动提交时,无法使用 atomic()

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() 不会被撤销。

返回顶部