Как использовать транзакции в Django ORM

Ответ

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

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

1. Декоратор transaction.atomic

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

from django.db import transaction, IntegrityError

@transaction.atomic
def create_user_with_profile(username, first_name):
    user = User.objects.create(username=username)
    # Если здесь произойдет ошибка (например, IntegrityError),
    # создание пользователя также будет отменено.
    UserProfile.objects.create(user=user, first_name=first_name)

2. Контекстный менеджер transaction.atomic

Позволяет обернуть в транзакцию только определённый блок кода, а не всю функцию. Это даёт больше гибкости.

from django.db import transaction

def update_balance():
    try:
        with transaction.atomic():
            # Блок кода, который должен быть атомарным
            account_from = Account.objects.select_for_update().get(id=1)
            account_to = Account.objects.select_for_update().get(id=2)

            account_from.balance -= 100
            account_to.balance += 100

            account_from.save()
            account_to.save()
    except Account.DoesNotExist:
        # Обработка ошибки, транзакция уже отменена
        print("Счет не найден.")

Ключевые моменты:

  • @transaction.atomic и with transaction.atomic() — основные и предпочтительные способы управления транзакциями.
  • select_for_update() используется для пессимистичной блокировки строк на время транзакции, чтобы предотвратить их изменение другими процессами.
  • Вложенные блоки atomic создают точки сохранения (savepoints), позволяя откатывать только часть операций.

Ответ 18+ 🔞

А, ну вот, подъехали к самому сокровенному — к тому, как не наебнуться с данными в Django. Смотри, тут всё просто, как три копейки, но если проебаться — будет пиздец, а не целостность.

По дефолту эта ORM ведёт себя как распиздяй: каждый запрос к базе — это отдельная, мелкая транзакция, которая тут же коммитится (autocommit). Это как покупать продукты по одной картошине, выходя из магазина после каждой. Хуёво, да? А если тебе надо купить ингредиенты для борща, и если хоть чего-то нет — то нахуй весь борщ? Вот для этого и нужны нормальные транзакции.

1. Декоратор @transaction.atomic — для ленивых и умных

Вешаешь волшебный декоратор на свою функцию — и вуаля, весь её код становится одним неделимым куском для базы. Всё выполнится, либо, в случае любой ебли, всё откатится, как будто ничего и не было. Красота.

from django.db import transaction, IntegrityError

@transaction.atomic
def create_user_with_profile(username, first_name):
    user = User.objects.create(username=username)
    # Если тут, блядь, вылезет IntegrityError (логин занят, например),
    # то и юзер, которого только что создал, нахуй испарится. Чистота.
    UserProfile.objects.create(user=user, first_name=first_name)

Представь, ты создал юзера, а потом профиль не встал из-за дурацкого условия. Без атомика у тебя в базе болтался бы полуфабрикат — юзер без профиля. А так — нихуя. Всё чисто.

2. Контекстный менеджер with transaction.atomic() — для точечных операций

А что, если тебе не всю функцию в транзакцию пихать, а только её сочный кусок? Вот тут и выручает контекстный менеджер. Зашёл в блок with — ты в транзакции. Вышел — либо всё ок, либо всё посралось.

from django.db import transaction

def update_balance():
    try:
        with transaction.atomic():
            # Вот этот кусок кода — священная корова. Либо всё, либо ничего.
            # Берём два счёта и нахуй их блокируем, чтобы никто другой не влез.
            account_from = Account.objects.select_for_update().get(id=1)
            account_to = Account.objects.select_for_update().get(id=2)

            account_from.balance -= 100
            account_to.balance += 100

            account_from.save()
            account_to.save()
    except Account.DoesNotExist:
        # Если счёта нет — транзакция уже откатилась сама, можно просто ругнуться.
        print("Ёпта, счёт не найден. Деньги уплыли в никуда.")

На что тут смотреть, чтобы не обосраться:

  • @transaction.atomic и with transaction.atomic() — твои лучшие друзья. Используй их, когда делаешь несколько связанных изменений. Не выёбывайся.
  • select_for_update() — это, блядь, очень важно в таких сценариях, как перевод денег. Эта штука говорит базе: "Слушай, эти строки я сейчас буду трогать, залочь их нахуй, чтобы никакой другой процесс параллельно не проебал мою логику". Без этого возможны гонки, и деньги могут волшебным образом испариться или удвоиться. Пиздец, а не фича.
  • Если ты вложишь один atomic в другой — это создаст точки сохранения (savepoints). Это как если в большой транзакции ты можешь откатить не всё, а только до какой-то контрольной точки. Удобно, но не усложняй без нужды.

Короче, суть в чём: хочешь быть уверен, что твои данные не превратятся в кашу — оборачивай логически связанные операции в atomic. И не забывай про блокировки, когда несколько процессов могут лезть в одни и те же записи. Иначе будет овердохуища веселья при отладке.