Зарегистрируйтесь, чтобы продолжить обучение

Транзакции Python: Django ORM

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

В курсе по основам реляционных баз данных вы уже встречались с понятием транзакций и набором требований к транзакционным системам под названием 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 в конкретной СУБД.

Из сказанного выше стоит сделать следующий вывод: один уровень транзакций работает везде и всегда (практически), а все виды вложенности требуют внимательного отслеживания того, с какими СУБД будет работать проект.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 27 февраля

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»