数据库事务

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)

原子性是数据库事务的定义属性。 atomic 允许创建代码块来保证数据库的原子性。如果代码块成功创建,这个变动会提交到数据库。如果有异常,变动会回滚。

atomic 块可以嵌套。在这个例子里,当内部块成功完成时,如果在稍后外部块里引发了异常,则仍可回滚到最初效果。

atomic 既可用作 decorator:: :

from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

也可用作 context manager:: :

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()

在 try/except 块中使用装饰器 atomic 来允许自然处理完整性错误:

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() 的变化也会在这里,并且绑定到相同的事务。注意,任何试图在 generate_relationships() 中执行的操作在 handle_exception() 被调用的时候也会安全的回滚,因此异常处理也会在必要的时候在数据库上操作。

要避免在 atomic 内部捕捉异常!

当存在 atomic 块时, Django 查看它是否正常退出或存在异常来决定是提交还是正常回滚。如果你在 atomic 内部捕捉并且处理异常,你可以对 Django 隐藏问题代码。这会导致一些意外的行为。

这主要是 DatabaseError 和它的子类的一个问题(比如 IntegrityError )。出现这样的错误之后,事务会奔溃,并且 Django 将在 atomic 块的末尾执行回滚。如果你打算在回滚发生的时候运行数据库查询,Django 将引发 TransactionManagementError 错误。当 ORM 相关的信号处理程序引发异常时,你也可能遇到这个问题。

捕捉数据库错误的正确的方法是像上方所示那样围绕 atomic 块。如有需要,为此目的可以添加额外的 atomic 块。这个模式有别的优势:如果异常发生,它会明确界定哪些操作将回滚。

如果捕获由原始SQL查询引发的异常,那么Django的行为是未指定的,并且依赖于数据库。

当回滚事务时,你可能需要手工恢复模型状态。

当事务回滚时,模型字段的值不会被恢复。除非你手工恢复初始的字段值,否则这会导致模型状态不一致。

例如,给定带有 active 字段的 MyModel 模型,如果在事务中更新 activeTrue 失败,那么这个片段确保最后的 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:
    ...

为了保证原子性,atomic 禁用了一些API。在 atomic 块中试图提交、回滚或改变数据库连接的自动提交状态将引发异常。

atomic 带有 using 参数,这个参数是数据库名字。如果这个参数没有提供,Django 会使用默认数据库。

在后台,Django 的事务管理代码:

  • 当进入最外面的 atomic 块时打开事务;
  • 当进入 atomic 块内部时创建一个保存点;
  • 从块内部退出时释放或回滚保存点;
  • 离开块的最外层时提交或回滚事务。

你可以通过设置 savepoint 参数为 False 来为内部块禁用保存点的创建。如果发生异常,Django将在退出带有保存点的第一个父块(如果有的话)时执行回滚,否则退出最外面的块。外部事物仍保证了原子性。仅当保存点开销明显时,才应使用此选项。它的缺点是破坏了上述错误处理。

当自动提交关闭时,可以使用 atomic 。它将只使用保存点,即使对于最外面的块也是如此。

性能考虑因素

打开事务会对数据库服务器有性能成本。尽量减少这种开销,要保持事务尽可能简短。如果正在 Django 的请求 / 响应周期之外,在长时间运行的进程中使用 atomic() ,这点尤其重要。

自动提交

为什么 Django 使用自动提交

在 SQL 规范中,每一个 SQL 查询会启动事务,除非一个事务已经处于活动状态。然后必须显式地提交或回滚此事务。

这对开发者来说一直很头疼。为了减轻这个问题,大部分数据库提供了自动提交模式。当打开了自动提交,并且没有事务活动时,每一个 SQL 查询将被包含在自己的事务中。换句话说,每一个这种查询不仅会启动一个事务,而且事务也会被自动提交或回滚,这取决于查询是否成功。

PEP 249 (Python 数据库接口规范 v2.0)要求自动提交在初始时是关闭的。Django 会覆盖这个默认值并开启自动提交。

为了避免这种情况,你可以参考 deactivate the transaction management<deactivate-transaction-management> ,但并不推荐这样做。

停用事务管理

你可以通过设置 AUTOCOMMITFalse 来对数据库完全禁用 Django 事务管理。如果你这么做了,Django 将不会启动自动提交,而且不会执行任何提交。你将获得底层数据库的常规行为。

这要求你显式地提交每一个事务,即使它们通过 Django 或第三方库启动。因此,这适用于当你想运行事务控制中间件或做一些非常奇怪的事情的情形。

提交后

有时你需要执行与当前数据库事务相关的操作,但前提是事务成功提交。例子可能包含 Celery  任务,邮件提醒或缓存失效。

Django provides the on_commit() function to register callback functions that should be executed after a transaction is successfully committed:

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() 回调不会运行。如果你需要测试 on_commit() 回调的结果,可以改用 TransactionTestCase

为什么没有事务回滚钩子?

事务回滚钩子相比事务提交钩子更难实现,因为各种各样的情况都可能造成隐式回滚。

比如,如果数据库连接被删除,因为进程被杀而没有机会正常关闭,回滚钩子将不会运行。

解决方法是:与其在执行事务时(原子操作)进行某项操作,当事务执行失败后再取消这项操作,不如使用 on_commit() 来延迟该项操作,直到事务成功后再进行操作。毕竟事务成功后你才能确保之后的操作是有意义的。

底层API

警告

应该尽可能使用 atomic() 。它说明了每个数据库的特性,并防止了无效操作。

底层API只在实现事务管理时有用。

自动提交

Django provides an API in the django.db.transaction module to manage the autocommit state of each database connection.

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

这些函数使接受一个 using 参数表示所要操作的数据库。如果未提供,则 Django 使用 "default" 数据库。

自动提交默认为开启,如果你将它关闭,自己承担后果。

一旦你关闭了自动提交, Django 将无法帮助你,数据库将会按照你使用的数据库适配器的默认行为进行操作。虽然适配器的标准经过了 PEP 249 详细规定,但不同适配器的实现方式并不总是一致的。你需要谨慎地查看你所使用的适配器的文档。

在关闭自动提交之前,你必须确保当前没有活动的事务,通常你可以执行 commit() 或者 rollback() 函数以达到该条件。

当一个原子 atomic() 事务处于活动状态时, Django 将会拒绝关闭自动提交的请求,因为这样会破坏原子性。

事务

事务是指具有原子性的一系列数据库操作。即使你的程序崩溃,数据库也会确保这些操作要么全部完成要么全部都未执行。

Django doesn't provide an API to start a transaction. The expected way to start a transaction is to disable autocommit with 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 会在保存点语句之前隐式提交。(事实上,它会在除了 SELECT, INSERT, UPDATE, DELETE and REPLACE 之前的任何语句之前提交)这个 Bug 有两个后果:

  • 保存点的底层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() 做的改变会丢失,即使操作本身没有引发错误。

保存点回滚

你可以使用 savepoints 来控制回滚的程度。执行可能失败的数据库操作之前,你可以设置或更新保存点;这样,如果操作失败,你可以回滚单一的错误操作,而不是回滚整个事务。比如:

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

在这个例子里, a.save() 将不会在 b.save() 引发异常的情况下被撤销。

Back to Top