Транзакции БД

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

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

Поведение транзакции в Django по умолчанию

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

Django использует транзакции или точки сохранения автоматически, чтобы гарантировать целостность операций ORM, которые требуют нескольких запросов, особенно delete() и :ref:`update() <topics-db-queries-update>`запросы.

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

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

Распространенный способ обработки транзакций в Интернете - заключать каждый запрос в транзакцию. Установите ATOMIC_REQUESTS в True в конфигурации каждой базы данных, для которой вы хотите включить это поведение.

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

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

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

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

Транзакции по запросу и потоковые ответы

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

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

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

Обратите внимание, что только выполнение вашего представления включено в транзакции. Промежуточное программное обеспечение (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, durable=False)[исходный код]

Атомарность является определяющим свойством транзакций базы данных. atomic позволяет нам создать блок кода, внутри которого гарантируется атомарность в базе данных. Если блок кода успешно завершен, изменения фиксируются в базе данных. Если есть исключение, изменения отменяются.

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

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

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

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

Избегайте перехвата исключений внутри atomic!

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

В основном это касается DatabaseError и его подклассов, таких как IntegrityError. После такой ошибки транзакция прерывается, и Django выполнит откат в конце «атомарного» блока. Если вы попытаетесь выполнить запросы к базе данных до того, как произойдет откат, 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 отключает некоторые API. Попытка зафиксировать, откатить или изменить состояние автоматической фиксации соединения с базой данных в блоке atomic вызовет исключение.

atomic принимает аргумент using, который должен быть именем базы данных. Если этот аргумент не указан, Django использует базу данных 'default'.

Под капотом код управления транзакциями Django:

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

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

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

Вопросы производительности

Открытые транзакции снижают производительность вашего сервера базы данных. Чтобы минимизировать эти накладные расходы, сделайте ваши транзакции как можно короче. Это особенно важно, если вы используете atomic() в длительных процессах вне цикла запросов/ответов Django.

Changed in Django 4.1:

В старых версиях проверка на долговечность была отключена в django.test.TestCase.

Автокоммит

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

В стандартах SQL каждый запрос SQL запускает транзакцию, если только он не активен. Такие транзакции затем должны быть явно зафиксированы или отменены.

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

PEP 249, the Python Database API Specification v2.0, requires autocommit to be initially turned off. Django overrides this default and turns autocommit on.

Чтобы избежать этого, вы можете деактивировать управление транзакциями, но это не рекомендуется.

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

Вы можете полностью отключить управление транзакциями в Django для данной базы данных, установив AUTOCOMMIT в False в его конфигурации. Если вы сделаете это, 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().

Changed in Django 4.2:

Добавлен аргумент robust.

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

Точки сохранения (то есть вложенные блоки 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, зарегистрированная с помощью robust=False в данной транзакции, вызовет невыловленное исключение, то ни одна из последующих зарегистрированных функций в этой же транзакции не будет запущена. Это то же самое поведение, как если бы вы сами последовательно выполняли функции без on_commit().

Changed in Django 4.2:

Добавлен аргумент robust.

Сроки исполнения

Ваши обратные вызовы выполняются после успешной фиксации, поэтому сбой в обратном вызове не приведет к откату транзакции. Они выполняются условно при успешном завершении транзакции, но не являются частью транзакции. Для предполагаемых случаев использования (почтовые уведомления, фоновые задачи и т.д.) это должно быть нормально. Если же это не так (если ваше последующее действие настолько критично, что его отказ должен означать отказ самой транзакции), то не стоит использовать хук on_commit(). Вместо него можно использовать two-phase commit, например psycopg Two-Phase Commit protocol support и optional Two-Phase Commit Extensions in the Python DB-API specification.

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

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

Функции on-commit работают только с режимом автоматической фиксации и API транзакции atomic() (или ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS> `). Вызов :func:`on_commit, когда автокоммит отключен, а вы не находитесь в атомарном блоке, приведет к ошибке.

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

Класс Django TestCase оборачивает каждый тест в транзакцию и откатывает эту транзакцию после каждого теста, чтобы обеспечить изоляцию теста. Это означает, что никакая транзакция на самом деле никогда не фиксируется, поэтому ваши обратные вызовы on_commit() никогда не будут запущены.

Вы можете преодолеть это ограничение, используя TestCase.captureOnCommitCallbacks(). Это фиксирует ваши обратные вызовы on_commit() в списке, позволяя вам делать утверждения по ним или имитировать фиксацию транзакции, вызывая их.

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

Почему нет хука отката?

Хук отката сложнее реализовать надежнее, чем хук фиксации, так как множество вещей может вызвать неявный откат.

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

Но есть решение: вместо того, чтобы что-то делать во время атомарного блока (транзакции), а затем отменить его в случае сбоя транзакции, используйте on_commit(), чтобы отложить это в первую очередь до тех пор, пока транзакция не завершится успешно. Гораздо проще отменить то, чего ты никогда не делал!

API низкого уровня

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

Всегда предпочитайте atomic(), если это вообще возможно. Он учитывает особенности каждой базы данных и предотвращает недопустимые операции.

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

Автокоммит

Django предоставляет API в модуле django.db.transaction для управления состоянием автоматической фиксации каждого соединения с базой данных.

get_autocommit(using=None)[исходный код]
set_autocommit(autocommit, using=None)[исходный код]

Эти функции принимают аргумент using, который должен быть именем базы данных. Если это не предусмотрено, Django использует базу данных 'default'.

Автокоммит изначально включен. Если вы выключите его, это ваша ответственность, чтобы восстановить его.

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

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

Django откажется отключать автокоммит, когда активен блок atomic(), потому что это нарушит атомарность.

Финансовые транзакции

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

Django не предоставляет API для запуска транзакции. Ожидаемый способ начать транзакцию - отключить автокоммит с помощью set_autocommit().

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

commit(using=None)[исходный код]
rollback(using=None)[исходный код]

Эти функции принимают аргумент using, который должен быть именем базы данных. Если это не предусмотрено, Django использует базу данных 'default'.

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

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

Точка сохранения - это маркер внутри транзакции, который позволяет вам откатить часть транзакции, а не полную транзакцию. Точки сохранения доступны с бэкэндами SQLite, PostgreSQL, Oracle и MySQL (при использовании механизма хранения InnoDB). Другие бэкэнды предоставляют функции точек сохранения, но они являются пустыми операциями - на самом деле они ничего не делают.

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

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

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

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

savepoint(using=None)[исходный код]

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

savepoint_commit(sid, using=None)[исходный код]

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

savepoint_rollback(sid, using=None)[исходный код]

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

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

Кроме того, есть полезная функция:

clean_savepoints(using=None)[исходный код]

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

В следующем примере демонстрируется использование точек сохранения:

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 неявно фиксируется перед инструкциями savepoint. (На самом деле, он фиксируется перед любым оператором, кроме SELECT, INSERT, UPDATE, DELETE и REPLACE.) Эта ошибка имеет два следствия:

  • 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

Вызов action.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() вызывает исключение.

Вернуться на верх