До сих пор нам встречался только код, работающий с одной сущностью за раз. В тех же примерах, когда создавались или изменялись несколько сущностей одновременно, никак не учитывалась ситуация, при которой посреди процесса обработки возникает ошибка и часть данных успевает перейти в новое состояние, а оставшиеся сущности остаются в старом. В таких случаях говорят: "нарушена консистентность".
В курсе по основам реляционных баз данных вы уже встречались с понятием транзакций и набором требований к транзакционным системам под названием ACID. Большинство СУБД, с которыми Django умеет работать, этим требованиям в той или иной форме соответствуют, а Django ORM предоставляет средства для управления транзакциями.
Тут стоит сразу отметить, что по умолчанию ORM использует режим "autocommit" и все запросы сразу применяются к БД. Об этой особенности стоит помнить и явно использовать транзакции в тех местах, где консистентность может быть нарушена.
Использование atomic
Для того, чтобы пометить фрагмент кода, как относящийся к одной транзакции, обычно используют менеджер контекста atomic():
from django.db import transaction
from django.core.exceptions import ValidationError
def process_order(order_items):
# Открываем транзакцию
with transaction.atomic():
# Создаем заказ
order = Order.objects.create(total=0, status='pending')
total_price = 0
for item in order_items:
product = Product.objects.select_for_update().get(id=item['product_id'])
# Проверяем наличие товара
if product.stock < item['quantity']:
raise ValidationError(f"Недостаточно товара {product.name} в наличии")
# Уменьшаем количество товара
product.stock -= item['quantity']
product.save()
total_price += product.price * item['quantity']
# Обновляем итоговую сумму заказа
order.total = total_price
order.status = 'confirmed'
order.save()
Умение менеджера контекста реагировать на ошибки, возникающие в его теле, здесь приходится весьма кстати. В результате при первой же ошибке вся транзакция отменяется. Так мы можем выполнять опасные операции, защищая базу данных от неконсистентности.
Разработчики на Django часто хотят выполнить в транзакции весь код какой-либо view
. При этом atomic()
используют как декоратор:
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# Вся view работает в рамках транзакции
do_stuff()
В модуле django.db.transaction
присутствуют две функции: commit()
и rollback()
. Вызов первой применяет накопленные в рамках транзакции изменения к базе данных. Вызов второй отбрасывает эти изменения, как будто транзакции и вовсе не было. Когда atomic()
ловит ошибки, выполняется rollback()
, а если же выполнение обёрнутого кода завершилось успешно, то делается commit()
. Поэтому думать об использовании этих функций обычно не приходится. Полезны эти функции могут быть тогда, когда в рамках блока менеджера контекста без возникновения ошибки становится понятно, что транзакция должна быть завершена так или иначе:
from django.db import transaction
from decimal import Decimal
def process_payment(order, payment_amount):
with transaction.atomic():
# Получаем актуальное состояние заказа из базы
order.refresh_from_db()
# Проверяем, не оплачен ли уже заказ
if order.status == 'paid':
# Явно откатываем транзакцию, так как заказ уже оплачен
transaction.set_rollback(True)
return "Заказ уже оплачен"
# Проверяем сумму платежа
if payment_amount < order.total:
# Сумма меньше необходимой - откатываем транзакцию
transaction.set_rollback(True)
return "Недостаточная сумма платежа"
if payment_amount > order.total:
# Если сумма больше - создаем возврат излишка
refund_amount = payment_amount - order.total
Refund.objects.create(
order=order,
amount=refund_amount
)
# Обновляем статус заказа
order.status = 'paid'
order.save()
return "Платеж успешно обработан"
Точки сохранения
Параллельно с транзакциями существуют ещё и точки сохранения (savepoints). Они обычно используются в рамках транзакции и отмечают места, в которых текущее состояние считается консистентным. После создания точки сохранения, к запомненному там состоянию можно вернуться и отбросить таким образом изменения, произошедшие после создания savepoint. Точки сохранения могут быть созданы внутри транзакции и откат к некоторой savepoint не приводит к откату всей транзакции. Можно воспринимать savepoints как некий аналог Undo в редакторе, тогда как транзакция скорее похожа на сохранение всего файла или выход из редактора без сохранения.
Функция savepoint()
из модуля django.db.transaction
создает точку сохранения, возвращая её идентификатор ("savepoint id" или "sid").
Функция savepoint_commit(sid)
сохраняет изменения, произошедшие с момента создания соответствующей savepoint. А функция savepoint_rollback(sid)
откатывает изменения к тому состоянию, которые имела база на момент создания соответствующей savepoint. Тот факт, что при вызове упомянутых двух функций указывается "sid", говорит о том, что откатывать изменения можно к любой из ранее созданных точек сохранения (в пределах транзакции).
from django.db import transaction
def process_complex_order(order, items):
with transaction.atomic():
order.status = 'processing'
order.save()
# Создаем точку сохранения перед обработкой товаров
items_sid = transaction.savepoint()
try:
for item in items:
product = Product.objects.select_for_update().get(id=item.product_id)
if product.stock < item.quantity:
# Если товара недостаточно, откатываемся к началу обработки товаров
transaction.savepoint_rollback(items_sid)
raise ValueError(f"Недостаточно товара: {product.name}")
product.stock -= item.quantity
product.save()
# Если все товары обработаны успешно, подтверждаем эту часть
transaction.savepoint_commit(items_sid)
# Создаем новую точку сохранения перед обработкой оплаты
payment_sid = transaction.savepoint()
try:
process_payment(order)
transaction.savepoint_commit(payment_sid)
order.status = 'completed'
order.save()
except PaymentError:
# Откатываемся только к началу обработки оплаты
transaction.savepoint_rollback(payment_sid)
order.status = 'payment_failed'
order.save()
except ValueError:
# При проблеме с товарами вся транзакция откатится автоматически
raise
Вложенные транзакции
Если внутри кода, уже обёрнутого в вызов atomic()
, в том или ином виде будет использован ещё один вызов atomic()
, то ORM создаст точку сохранения, вместо ещё одной транзакции. Откат таких "вложенных транзакций" не откатывает внешнюю транзакцию.
Ограничения
Транзакции поддерживаются абсолютным большинством СУБД, среди тех, с которыми Django умеет работать. Но этого же нельзя сказать о savepoints. Если конкретная СУБД в принципе не поддерживает точки сохранения, то любой код, который их использует, будет сразу вносить изменения в БД, а откат работать перестанет. И если при явном использовании savepoint()
и прочих специфичных функций программист обычно знает о том, что делает, то использование вложенных вызовов atomic()
может неприятно удивить, если забыть об отсутствии savepoints в конкретной СУБД.
Из сказанного выше стоит сделать следующий вывод: один уровень транзакций работает везде и всегда (практически), а все виды вложенности требуют внимательного отслеживания того, с какими СУБД будет работать проект.

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.