Управление транзакциями базы данных

Django предоставляет несколько инструментов для управления транзакциями базы данных.

Управление транзакциями

Стандартное поведение Django

По умолчанию Django использует режим автоматической фиксации(«autocommit»). Каждый запрос сразу фиксируется в базе данных, если не используется транзакция. Смотрите ниже.

Django автоматически использует транзакции или точки сохранения для операций ORM, которые требуют нескольких запросов, особенно для delete() и update().

Класс TestCase также оборачивает каждый тест в транзакцию для оптимизации скорости выполнения тестов.

Привязка транзакций к HTTP запросам

Обычной практикой для Web-приложений является оборачивание каждого запроса в транзакцию. Чтобы активировать такое поведение, установите ATOMIC_REQUESTS в True для соответствующей базы данных.

Это работает следующим образом: при получении запроса Django начинает транзакцию. Если ответ был создан без возникновения ошибок, Django фиксирует все ожидающие транзакции. Если функция представления вызывает исключение, Django откатывает все ожидающие транзакции.

Вы можете выполнять частичную фиксацию или откат изменений в коде представления, обычно с помощью менеджера контекста atomic(). Но в конце выполнения представления будут зафиксированы все изменения, или не одно из них.

Предупреждение

Хотя такая обработка запросов выглядит очень простой и привлекательной, она может быть неэффективна при росте количества запросов. Создание транзакции для каждого запроса создает небольшую нагрузку на базу данных. Влияние на производительность сервера зависит от запросов в вашем приложении и от того, на сколько эффективно ваша база данных работает с блокировками.

Транзакции для запроса и потоковый ответ

Когда представление возвращает StreamingHttpResponse, чтение содержимого ответа выполняет некоторый код для его ответа. Т.к. представление уже отработало, этот код будет выполняться вне транзакции.

Не советуем выполнять запись в базу данных при генерации потокового ответа, т.к. нет хорошего способа обработать ошибки после начала отправки ответа.

In practice, this feature wraps every view function in the atomic() decorator described below.

Обратите внимание, только представления оборачиваются в транзакцию. Промежуточные слои(middleware) выполняются вне транзакции, аналогично выполняется и рендеринг ответа.

При включенной 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()

Обернув 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()

В этом примере, вы можете выполнить запросы в add_children(), даже если generate_relationships() вызывал ошибку, также никуда не денутся изменения, выполненные в create_parent(). Обратите внимание, любые операции из generate_relationships() уже будут отменены, и при обработке ошибки в handle_exception() можно безопасно выполнять запросы к базе данных.

Избегайте перехвата ошибок в atomic!

При выходе из блока atomic Django определяет произошла ли ошибка, чтобы понять фиксировать изменения или отменить их. Если вы перехватите исключение и обработаете его в блоке atomic, вы можете скрыть от Django наличие проблемы. Это может привести к непредвиденному поведению.

В основном это относится к DatabaseError и классам наследникам, например IntegrityError. При таких ошибках транзакция будет сломана и Django выполнит отмену изменений после завершения блока atomic. Если вы попытаетесь выполнить запросы перед отменой изменений, Django вызовет исключение TransactionManagementError. Подобное поведение может произойти, если обработчик сигналов ORM вызовет исключение.

Правильный способ обработать ошибки базы данных – перехватить их за блоком atomic, как в примере выше. Если необходимо, добавьте дополнительный блок atomic для этого. У такого подхода есть свои преимущества: он явно разделяет операции, которые должны быть отменены при ошибке.

Если вы перехватите исключение, вызванное чистым SQL запросом, поведение Django будет зависеть от вашей базы данных.

В некоторых случаях вам необходимо отменить изменения в модели после отката транзакции

Значения полей модели не будут изменены обратно после отката транзакции. Это может привести к не согласующемся состоянию модели, если вы явно не восстановите начальные значения.

Например, возьмем MyModel с полем active, этот код проверяет правильное значение в 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:
    ...

Чтобы гарантировать атомарность, atomic блокирует некоторые функции. При попытке явно зафиксировать или отменить изменения, или изменить статус «autocommit» подключения к базе данных внутри блока atomic, будет вызвано исключение.

atomic принимает аргумент using, который обозначает имя базы данных с которой производится работа. Если этот аргумент не указан, то все действия идут относительно стандартной ("default") базы данных.

Код обработки транзакций в Django выполняет следующие действия:

  • создает транзакцию при входе в блок atomic;
  • создает точку сохранения при входе во вложенный блок atomic;
  • сохраняет или отменяет точку сохранения при выходе из вложенного блока;
  • фиксирует или отменяет транзакцию при выходе из последнего блока.

Вы можете отменить создание точки сохранения для внутренних блоков, указав в аргументе savepoint False. При исключении Django выполнит отмену изменений при выходе из первого попавшегося блока, который создает точку сохранения, или же самого последнего блока. Атомарность внешних транзакций все также гарантирована. Такой подход должен использоваться только для оптимизации создания точек сохранения, т.к. нарушает обработку ошибок, как было описано выше.

Вы можете использовать atomic при выключенном «autocommit». Он использует только точки сохранения, даже для самого внешнего блока.

Заметка о производительности

Создание транзакции требует определенных операций от базы данных. Чтобы минимизировать нагрузку на сервер базы данных, делайте транзакции минимально короткими. Это особенно важно при использовании atomic() в долго выполняемых процессах вне цикла обработки запроса и ответа.

Autocommit

Почему Django использует автоматическую фиксацию

По стандартам SQL каждый SQL-запрос начинается с транзакции, если она еще не создана. Эти транзакции должны быть явно зафиксированы или отменены.

Это не всегда удобно при разработке приложения. Чтобы решить эту проблему, большинство баз данных поддерживает режим автоматической фиксации(autocommit). При включенном «autocommit», если транзакция не активна, каждый SQL запрос обернут в транзакцию. То есть транзакция для каждого запроса не только создается, но и автоматически фиксируется, если запросы был успешно выполнен.

PEP 249, спецификация Python о Database API v2.0, требует чтобы «autocommit» по умолчанию был выключен. Django переопределяет это поведение и «autocommit» по умолчанию включен.

Вы можете отключить такое поведение, но мы не рекомендует этого делать.

Отключение управления транзакциями

Вы можете отключить управление транзакциями Django для определенной базы данных, установив AUTOCOMMIT в False в её настройках. При этом Django отключит «autocommit» и не будет выполнять фиксирование изменений. Все будут работать в соответствии с библиотекой, которая используется для работы с базой данных.

При этом вам необходимо явно фиксировать каждую транзакцию, не зависимо была ли она создана Django, или сторонней библиотекой. Это может быть полезно, если вы хотите использовать свой промежуточный слой(middleware) для управления транзакциями, или сделать что-то очень необычное.

Выполнение операция после коммита

Иногда вам необходимо выполнить какие-либо действия связанные с текущей транзакцией в базе данных, но только при успешном коммите транзакции. Это может быть задача Celery, отправка электронного письма, или сброс кэша.

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() пока транзакция активна, обработчик сразу выполнится.

Если транзакция будет отменена (обычно при возникновении ошибки в блоке 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().

Время выполнения обработчиков

Your callbacks are executed after a successful commit, so a failure in a callback will not cause the transaction to roll back. They are executed conditionally upon the success of the transaction, but they are not part of the transaction. For the intended use cases (mail notifications, Celery tasks, etc.), this should be fine. If it’s not (if your follow-up action is so critical that its failure should mean the failure of the transaction itself), then you don’t want to use the on_commit() hook. Instead, you may want two-phase commit such as the psycopg Two-Phase Commit protocol support and the optional Two-Phase Commit Extensions in the Python DB-API specification.

Обработчики не вызываются, пока не будет восстановлен «autocommit» (т.к. любой запрос в обработчике неявно создаст транзакцию и заблокирует переход подключения в «autocommit» режим).

Если назначить обработчик в «autocommit» режиме вне блока atomic(), он будет выполнен моментально, а не после коммита.

Обработчики коммита транзакции работают только с «autocommit» режимом и atomic() (или при ATOMIC_REQUESTS) API транзакций. Вызвав on_commit() при отключенном «autocommit» и вне блока atomic приведет к ошибке.

Использование в тестах

Встроенный класс TestCase оборачивает каждый тест в транзакцию и отменяет её после выполнения теста, чтобы обеспечить изолированность тестов. Это означает, что ни одна транзакция никогда не коммитится, и обработчики on_commit() никогда не будут вызваны. Если вам нужно протестировать выполнение обработчика on_commit(), используйте TransactionTestCase.

Почему нет обработчика отмены транзакции?

Обработчик отмены транзакции реализовать на много сложнее т.к. причин отката на много больше.

Например, если подключение к базе данных оборвалось из-за внезапной остановки процесса, ваш обработчик отмены транзакции никогда не сможет выполниться.

But there is a solution: instead of doing something during the atomic block (transaction) and then undoing it if the transaction fails, use on_commit() to delay doing it in the first place until after the transaction succeeds. It’s a lot easier to undo something you never did in the first place!

Низкоуровневый API

Предупреждение

При любой возможности используйте atomic(). Эта функция учитывает особенности различных баз данных и предотвращает использование недоступных операций.

Низкоуровневый API полезен только при реализации собственного управления транзакциями.

Autocommit

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, который обозначает имя базы данных. Если этот аргумент не указан, то все действия идут относительно стандартной ("default") базы данных.

«autocommit» по умолчанию включен. Если вы выключили его, на вашей ответственности и его включение.

При выключении «autocommit», вы получаете поведение по умолчанию библиотеки, которая используется для работы с базой данных, и Django ничем вам не поможет. Это поведение описано в PEP 249, но реализация в различных библиотеках может отличаться. Вам следует изучить документацию используемой библиотеки.

Перед включением «autocommit» вы должны убедиться, что транзакция не активна, обычно выполнив commit() или rollback().

Django не позволит выключить «autocommit», если активен блок atomic(), т.к. это нарушит атомарность.

Транзакции

Транзакция – это атомарный набор запросов к базе данных. Даже если ваша программа аварийно завершится, база данных гарантирует, что будут сохранены все изменения, или ни одно из них.

Django не предоставляет API для явного создания транзакции. Чтобы создать транзакцию, отключите «autocommit» с помощью set_autocommit().

Оказавшись в транзакции, можно зафиксировать выполненные изменения, используя функцию commit(), или отменить их через функцию rollback(). Эти функции находятся в модуле django.db.transaction.

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

Каждая из этих функций принимает аргумент using, который обозначает имя базы данных. Если этот аргумент не указан, то все действия идут относительно стандартной ("default") базы данных.

Django не позволит зафиксировать или отменить изменения, если блок atomic() активен, т.к. это нарушит атомарность.

Точки сохранения

A savepoint is a marker within a transaction that enables you to roll back part of a transaction, rather than the full transaction. Savepoints are available with the SQLite, PostgreSQL, Oracle, and MySQL (when using the InnoDB storage engine) backends. Other backends provide the savepoint functions, but they’re empty operations – they don’t actually do anything.

Точки сохранения бесполезны, если вы используете стандартное поведение Django – «autocommit». Тем не менее, при создании транзакции через atomic(), каждая открытая транзакция выполняет ряд операций в базе данных, ожидая фиксации или отката транзакции. Если вы выполните откат транзакции, то будет выполнен откат всей транзакции. Точки сохранения предоставляют возможность выполнять частичный откат, вместо выполнения полного отката, который делается с помощью transaction.rollback().

При вложенных блоках atomic() создаются точки сохранения, которые позволяют выполнить частичную фиксацию или откат изменений. Мы настоятельно рекомендуем использовать atomic() вместо функций, указанных выше, но они в любом случае входят в публичный API и мы не собираемся удалять их.

Каждая из этих функция принимает аргумент using, который обозначает имя базы данных с которой производится работа. Если этот аргумент не указан, то все действия идут относительно стандартной ("default") базы данных.

Точки сохранения управляются тремя функциями из django.db.transaction:

savepoint(using=None)

Создаёт новую точку сохранения, обозначая точку в транзакции, отмечающую «хорошее» состояние. Возвращает ID (sid) точки сохранения.

savepoint_commit(sid, using=None)

Закрывает точку сохранения с указанным sid. Все изменения, выполненные до этой точки сохранения, будут добавлены в текущую транзакцию.

savepoint_rollback(sid, using=None)

Откатывает транзакцию до точки сохранения с указанным sid.

Эти функции ничего не делают, если точки сохранения не поддерживаются базой данных, или если база данных в режиме «autocommit».

Также Django предоставляет несколько дополнительных функций:

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

While SQLite supports savepoints, a flaw in the design of the sqlite3 module makes them hardly usable.

При включенном «autocommit» точки сохранения не имеют смысла. При отключенном – sqlite3 неявно фиксирует изменения перед созданием точки сохранения. (На самом деле фиксация выполняет перед любой операцией отличной от SELECT, INSERT, UPDATE, DELETE и REPLACE.) Этот баг приводит к следующим особенностям:

  • Низкоуровневый API для работы с точками сохранения работает только внутри транзакции, то есть в блоке atomic().
  • Невозможно использовать atomic() при выключенном «autocommit».

Транзакции в MySQL

При использовании MySQL поддержка транзакция зависит от версии движка базы данных и от используемого типа таблиц. (Под «типом таблицы» мы подразумеваем «InnoDB» или «MyISAM».) Особенности транзакций MySQL выходят за рамки данной статьи, но сайт MySQL содержит информацию о `транзакциях<http://dev.mysql.com/doc/refman/5.6/en/sql-syntax-transactions.html>.

Если ваша настройка MySQL не поддерживает транзакции, тогда Django будет работать в режиме автоматической фиксации: Операторы будут выполняться и сразу же фиксироваться. Если же ваша настройка MySQL поддерживает транзакции, Django будет обрабатывать транзакции как описано выше.

Обработка исключений в транзакциях PostgreSQL

Примечание

Этот раздел важен только при создании собственного управления транзакциями. Описанная проблема не возникнет при стандартном поведении Django и при использовании atomic().

Inside a transaction, when a call to a PostgreSQL cursor raises an exception (typically IntegrityError), all subsequent SQL in the same transaction will fail with the error «current transaction is aborted, queries ignored until end of transaction block». While the basic use of save() is unlikely to raise an exception in PostgreSQL, there are more advanced usage patterns which might, such as saving objects with unique fields, saving using the force_insert/force_update flag, or invoking custom 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

В этом примере a.save() не будет отменён, если b.save() вызовет исключение.