数据库事务

Django提供给你几种方法来控制如何管理数据库事务。

管理数据库事务

Django的默认事务处理行为

Django 的默认行为是运行在自动提交模式下。 任何一个查询都立即被提交到数据库中,除非激活一个事务。 详细信息请参考下文

Django 用事务或者保存点去自动的保证复杂ORM各种查询操作的统一性,尤其是 delete()update() 查询.

Django的TestCase类也由于性能原因将每个测试封装在一个事务中。

将事务绑定到HTTP请求

在web上一种简单处理事务的方式是把每个请求用事务包装起来。 如果你想启用这种行为,请在数据库的配置中设置ATOMIC_REQUESTSTrue

它是这样工作的。 在调用一个view里面的方法之前,django开始一个事务。 如果发出的响应没有问题,Django就会提交这个事务。 如果在view这里产生一个异常,Django就会回滚这次事务。

你可以使用视图代码中的保存点执行子事务,通常使用atomic()上下文管理器。 但是,在视图结束时,更改全部都将被提交或全部都不提交。

警告

虽然这种简洁的事物模型看上去很吸引人, 但要注意当流量增长时它会表现出较差的效率。 对每个视图开启一个事务是有所耗费的。 其对性能的影响依赖于应用程序对数据库的查询语句效率和数据库当前的锁竞争情况。

预请求事务和流式响应

当一个视图返回一个 StreamingHttpResponse时, 其读取的内容是由执行代码来产生的。 因为视图调用已经返回,这样代码在事务的外部运行。

一般而言,在产生一个流式响应时,不建议再进行写数据库的操作,因为在开始发送响应之后处理错误没有合理的方式。

在实际操作时,可以通过如下atomic()装饰器把这一功能简单地封装到视图函数上。

表示事务仅仅是在当前视图中有效, 诸如模板响应之类的中间件(Middleware)操作是运行在事务之外的。

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)[source]

原子性是由数据库的事务操作来界定的。 atomic允许我们在执行代码块时,在数据库层面提供原子性保证。 如果代码块成功完成, 相应的变化会被提交到数据库进行commit; 如果执行期间遇到异常,则会将该段代码所涉及的所有更改回滚。

atomic块可以嵌套。 在下面的例子里,使用with语句,当一个内部块完成后,如果某个异常在外部块被抛出,内部块上的操作仍然可以回滚(前提是外部块也被atomic装饰过)。

atomic 被用作decorator:

from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # 下面的代码在一个事务中执行。
    do_stuff()

atomic被用作 context manager:

from django.db import transaction

def viewfunc(request):
    # 下面的代码在自动提交模式下执行(Django的默认模式)。
    do_stuff()

    with transaction.atomic():
        # 下面的代码在一个事务中执行。
        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的行为是不明确的,而且是依赖于数据库的。

你可能需要在回滚事务时手动还原模型状态。

当事务回滚发生时,模型字段的值将不会被还原。 这可能导致模型状态不一致,除非你手动还原原始字段值。

例如,假设MyModel有一个active字段,下面的代码片段确保如果在事务中更新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代码块内试图commit、roll back或者更改数据库连接的autocommit的状态都会引发一个异常。

atomic接受一个using参数,它必须是数据库的名字。 如果这个参数没提供,Django默认使用"default"数据库。

在底层,Django的事务管理代码:

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

你可以通过设置savepoint参数为False来停止内层代码库中的保存点的创建。 如果异常发生,若有保存点Django会在退出第一层代码块时执行回滚,否则会在最外层的代码块上执行回滚。 原子性始终会在外层事物上得到保证。 这个选项应该仅仅在保存点开销很明显的情况下使用。 它的缺点是打破了上述错误处理的原则。

在autocommit关闭的情况下,你可以使用atomic 它只会使用保存点,即使是最外面的块。

性能考虑

所有打开的事务会对数据库带来性能成本。 要最小化这种开销,保持你的事务尽可能短。 如果你使用atomic()来执行长时间运行的进程,将其置于Django的请求/响应周期之外尤其重要。

自动提交

为什么Django使用自动提交

在SQL标准中, 每个SQL语句在执行时都会启动一个事务,除非已经存在一个事务了。 这样事务必须明确是提交还是回滚。

对应用程序开发者而言,这样非常不方便。 为了避免这个问题,大多数数据库提供了一个autocommit模式。 当 autocommit被打开并且事物处于活动状态时,每个SQL查询都可以看成是一个事务。 也就是说, 不但每个查询是每个事物的开始,而且每个事物会自动提交或回滚,这取决于该查询是否成功执行。

PEP 249, Python数据库API 规范v2.0, 需要将autocommit初试设置为关闭状态。 Django覆盖了这个默认规范并且将autocommit设置为 on.

要想避免这样, 你可以deactivate the transaction management, 但不建议这样做。

停用事务管理

你可以在配置文件里通过设置AUTOCOMMITFalse 完全关闭Django的事物管理。 如果这样做了,Django将不能启用autocommit,也不能执行任何 commits. 你只能遵照数据库层面的规则行为。

这就需要你对每个事物执行明确的commit操作,即使由Django或第三方库创建的。 因此,这最好只用于你自定义的事物控制中间件或者是一些比较奇特的场景。

提交后执行动作

有时你需要执行与当前数据库事务相关的操作,但只有事务成功提交时才执行。 常见的例子包括Celery任务、电子邮件通知或缓存无效。

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

on_commit(func, using=None)[source]

传递函数(不带参数)到on_commit()

from django.db import transaction

def do_something():
    pass  # 发送一封邮件、使cache失效、发起一个Celery任务等

transaction.on_commit(do_something)

你也可以将函数封装在一个lambda中:

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

传入的函数将在数据库写入操作成功提交并调用on_commit()时立即调用。

如果在没有活动事务的情况下调用on_commit(),则将立即执行回调。

如果假设的数据库写入被回滚(通常当在atomic()块中引发未处理的异常)时,你的函数将被丢弃,不会被调用。

保存点

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

with transaction.atomic():  # 外层的atomic,启动一个新的事务
    transaction.on_commit(foo)

    with transaction.atomic():  # 内层的atomic块,创建一个保存点
        transaction.on_commit(bar)

# 在离开最外层的代码库时,将先调用foo()然后调用bar()

另一种情况,当保存点被回滚(由于引发异常)时,内部调用将不被调用:

with transaction.atomic():  # 外层的atomic,启动一个新的事务
    transaction.on_commit(foo)

    try:
        with transaction.atomic():  # 内层的atomic块,创建一个保存点
            transaction.on_commit(bar)
            raise SomeError()  # 引发一个异常 — 中止保存点
    except SomeError:
        pass

# foo()将被调用,但是bar()不会

执行顺序

给定事务的提交函数按照注册顺序执行。

异常处理

如果给定事务中的一个提交函数引发未捕获的异常,那么该同一事务中的后续注册函数将不会运行。 这当然是与你没有on_commit()的顺序执行函数一样的行为。

执行时间

您的回调在成功提交后执行,因此回调失败不会导致事务回滚。 它们在事务成功时有条件地执行,但它们不是事务的部分 对于预期用例(邮件通知,芹菜任务等)),这应该是罚款。 如果不是(如果您的后续操作非常关键,以致其失败意味着交易本身的失败),那么您不想使用on_commit()钩子。 相反,您可能需要两阶段提交,例如psycopg两阶段提交协议支持和Python DB-API中的可选两阶段提交扩展说明书 T2>。

在提交后连接上的自动提交恢复之前,不会运行回调(因为否则回调中完成的任何查询将打开隐式事务,从而阻止连接返回到自动提交模式)。

当处于自动提交模式并且在atomic()块之外时,该函数将立即运行,而不是在提交。

提交函数仅适用于autocommit modeatomic()(或ATOMIC_REQUESTS)事务API。 当autocommit被禁用并且您不在原子块内时会调用on_commit()将导致错误。

在测试中使用

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

为什么没有回滚的hook?

回滚的hook比提交的hook更难健壮地实现,因为各种事情都可能导致隐式回滚。

例如,如果因为你的进程被杀死导致你的数据库连接被删除,从而没有机会正常关闭,而你的回滚hook将永远不会运行。

解决方案很简单:不是在原子块(事务)中执行某些操作并在事务失败时撤消它,而是使用on_commit()来延迟执行操作,直到事务成功为止。 撤消你从未做过的事情要容易得多!

低级API

警告

如果可能的话,尽量优先选择atomic()来控制事务。 它遵守数据库的相关特性并且防止了非法操作。

低级别 API仅仅用于你自定义的事务管理场景。

自动提交

django.db.transaction模块里,Django提供了一个简单的API, 用于管理每个数据库的自提交状态。

get_autocommit(using=None)[source]
set_autocommit(autocommit, using=None)[source]

这些函数使用了一个 using参数,参数的值是数据库的名字。 如果参数没有提供, Django使用 "default" 数据库。

Autocommit初始是打开的。 如果你把它关掉,那么你有义务恢复它。

一旦你把autocommit关掉,那么你得到就是数据库的默认行为,Django不会帮你做任何事。 虽然在 PEP 249有描述此规范,但数据库适配器的实现并不总与规范是一致的。 请仔细检查你当前正在使用的数据库适配器文档。

在把autocommit改回打开之前,你必须确保没有活跃的事务,通常是调用一个commit()或者rollback()

atomic()代码块处于活跃状态时,Django会拒绝将autocommit从on的状态调整为off,因为这样会破坏原子性。

事务

事务是一系列数据库语句的原子集。 即使程序在运行时崩溃了,数据库可以确保事物集中的所有变更要么都被提交,要么都被放弃。

Django 并没有提供一个API来开启一个事务。 开始事务的预期方式是将set_autocommit()设置为disable状态。

一旦你处于一个事物之中,你可以选择要么apply所有变更 commit()提交它,要么取消所有变化 rollback(). 这个函数功能是在 django.db.transaction定义的。

commit(using=None)[source]
rollback(using=None)[source]

这些函数使用了一个 using参数,参数的值是数据库的名字。 如果参数没有提供, Django使用 "default" 数据库。

当一个 atomic()程序块在运行状态,Django会拒绝 commit 或rollback操作,因为这些操作是自动的。

保存点¶ T0>

保存点是在事物执行过程中的一个标记,它可以让你执行回滚事物的一部分 ,而不是整个事物。 保存点在 SQLite (≥ 3.6.8), PostgreSQL, Oracle和MySQL (当使用InnoDB存储引擎时)是有效的。 在其他的数据库后端虽然也提供保存点的函数,但其实它们是空操作,实际不起任何作用。

如果你开启了autocommit,Savepoints并没有太多用处,因为这是Django的默认行为。 然而, 一旦你使用 atomic()开启了一个事物, 那么你所建立的一系列数据库操作将被视为一个整体,等待同时提交或回滚。 如果你触发了一个回滚,那么整个事物就要进行回滚。 Savepoints提供了更细粒度的回滚,而不是用 transaction.rollback()对整个事物进行回滚.

当嵌套使用 atomic()装饰器时, 它会创建 savepoint以允许部分提交或回滚。 强烈建议你使用 atomic()而不是下面描述的函数功能,当然他们也是公开API的一部分,并且现在也没有废除它们的计划。

每个函数都带一个 using参数,这个参数是你要操作的数据库的名字。 如果没有 "default"参数,则会使用 using 数据库。

Savepoints 是由django.db.transaction里的三个函数来控制的:

savepoint(using=None)[source]

创建一个新的保存点。 这将实现在事物里对“好”的状态做一个标记点。 返回值是 savepoint ID (sid).

savepoint_commitsidusing = None[source]

释放保存点sid. 自创建保存点进行的更改将成为事物的一部分。

savepoint_rollbacksidusing = None[source]

回滚事物保存点sid.

如果不支持保存点或者数据库未处于autocommit模式,这些函数将什么也不做。

此外,还有一个实用的功能:

clean_savepoints(using=None)[source]

重置用来生成唯一保存点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()块中这么干的话, 那整个块都会被回滚,因为Django并不知你在下一层做此处理操作。 为了避免这样,你可以在下面函数中控制回滚行为。

get_rollback(using=None)[source]
set_rollback(rollback, using=None)[source]

当退出最内层atomic块时设置回滚标记为 True以实现强制回滚。 这对在没有抛出异常的情况下触发一个回滚操作是很有用的。

将标志设为 False 阻止这样一个回滚。 在此之前,请确保你已经把事物回滚到了该原子块内一个已知良好的保存点。 否则,你打破原子性,并且数据损坏可能会发生。

各个数据库的注意事项

SQLite中的保存点

SQLite ≥ 3.6.8之后开始支持savepoints,但由于sqlite3模块的设计缺陷导致其很难使用。

当自动提交被启用,保存点是没有意义的. 当被禁用时, sqlite3在savepoint之前已经进行了的隐式的提交。 (实际上, 在任何诸如INSERT, DELETE, UPDATE, REPLACESELECT等操作语句之前都会进行提交.) 这个问题有两个后果:

  • savepoint的低级别API只能用于内部事物。 在一个atomic() 块之内。
  • 当autocommit处于关闭状态时,是不可能使用atomic() 的。

MySQL中的事务

如果你使用MySQL,则表可能支持或可能不支持事务;这取决于你的MySQL版本和你使用的表类型。 (这里的表类型是指“InnoDB” 或 “MyISAM”等。) MySQL的事务特性不在本文讨论的范围之内,你可以从MySQL的官方站点获取MySQL事务的相关信息

如果你安装的MySQL 支持事务, 那么Django会一直工作在自动提交模式 : 语句一旦被调用就会被执行和提交。 如果你安装的MySQL确定支持事务,Django会遵循本文所介绍的关于事务的处理原则。

在PostgreSQL事务处理异常

本节内容只有当你实现你自己的事务管理时才相关。 这个问题不会出现在 Django默认模式和 atomic() 自动控制的情况下。

在一个事物内部, 当调用一个PostgreSQL光标抛出一个异常 (通常是 IntegrityError), 后续所有在此同一个事物中的SQL将失败并报以下错误“current transaction is aborted, queries ignored until end of transaction block”. 虽然简单使用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

在此例中, 当b.save()抛出异常的情况下,a.save() 所做的更改将不会丢失。