← Назад к вопросам

Как работают транзакции в Django?

2.2 Middle🔥 221 комментариев
#Django#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Транзакции в Django ORM

Транзакция — это атомарная последовательность операций с БД. Либо все выполняются успешно (COMMIT), либо все откатываются при ошибке (ROLLBACK). Это гарантирует целостность данных.

ACID свойства транзакций

  • Atomicity — либо всё, либо ничего
  • Consistency — всегда валидное состояние БД
  • Isolation — одновременные транзакции не мешают друг другу
  • Durability — коммиченные данные сохранены на диске

Базовый контроль транзакций

1. Автокоммит (по умолчанию)

from django.db import models

# По умолчанию Django автоматически коммитит после каждого запроса
user = User.objects.create(username='john', email='john@example.com')
# Автоматический commit в конце запроса (в представлении)

2. Context Manager: transaction.atomic()

from django.db import transaction

# Выполнить несколько операций атомарно
with transaction.atomic():
    user = User.objects.create(username='john', email='john@example.com')
    Profile.objects.create(user=user, bio='Developer')
    # Если вторая операция кинет исключение —
    # обе операции откатятся, user не будет создан

# При нормальном выходе из блока — commit

3. Декоратор @transaction.atomic()

from django.db import transaction

@transaction.atomic
def transfer_money(from_account, to_account, amount):
    """Транзакция гарантирует, что либо оба счета обновятся, либо ни один"""
    from_account.balance -= amount
    from_account.save()  # update 1
    
    to_account.balance += amount
    to_account.save()  # update 2
    
    # Обе операции откатятся, если вторая save() кинет исключение

# Вызов
transfer_money(account_a, account_b, 100)

Уровни изоляции (Isolation Levels)

# READ COMMITTED (по умолчанию в PostgreSQL)
# Видишь коммиченные данные, но не видишь изменения других транзакций

with transaction.atomic():
    user = User.objects.get(id=1)
    # Другая транзакция изменила этого пользователя
    user.refresh_from_db()  # Прочитать заново из БД
    print(user.email)  # Новый email

# REPEATABLE READ (в PostgreSQL и MySQL)
# Транзакция видит снимок данных на момент начала
# Не видит изменения других транзакций

with transaction.atomic():
    count1 = User.objects.count()
    # Другая транзакция добавила пользователей
    count2 = User.objects.count()
    # count1 == count2 (видим снимок на начало транзакции)

# SERIALIZABLE (строгая изоляция)
# Транзакции выполняются последовательно, как будто в одном потоке

Проблемы при параллельных транзакциях

1. Dirty Read (грязное чтение)

# Транзакция 1 изменяет, но не коммитит
# Транзакция 2 видит некоммиченные изменения
# Если транзакция 1 откатится — данные невалидны

# Решение: использовать READ COMMITTED (по умолчанию)

2. Lost Update (потеря обновления)

# ПЛОХО: race condition

# Процесс 1
user = User.objects.get(id=1)
user.balance += 50
user.save()  # балл: 150

# Одновременно Процесс 2
user = User.objects.get(id=1)
user.balance += 50
user.save()  # балл: 150 (потеря одного +50!)

# ХОРОШО: использовать F() для атомарного обновления
from django.db.models import F

User.objects.filter(id=1).update(balance=F('balance') + 50)
# Обновление происходит в БД, избегая race condition

3. Phantom Read (фантомное чтение)

# Транзакция 1 читает пользователей с balance > 100
# Транзакция 2 добавляет нового пользователя с balance > 100
# Транзакция 1 видит нового пользователя (фантом)

SELECT FOR UPDATE (Блокировка строк)

from django.db import transaction

# Заблокировать строку, чтобы другие транзакции ждали
with transaction.atomic():
    user = User.objects.select_for_update().get(id=1)
    
    # Пока эта транзакция открыта, никто не может
    # обновить или заблокировать этого пользователя
    
    user.balance -= 50
    user.save()
    # Блокировка снимается при COMMIT

# NOWAIT — не ждать, если строка заблокирована
with transaction.atomic():
    try:
        user = User.objects.select_for_update(nowait=True).get(id=1)
    except transaction.TransactionManagementError:
        print("Строка заблокирована другой транзакцией")

# SKIP LOCKED — пропустить заблокированные строки
users = User.objects.select_for_update(skip_locked=None).filter(status='pending')[:10]
# Возвращает только разблокированные строки (используется в очередях)

Пример: Банковский трансфер (атомарный)

from django.db import transaction
from django.db.models import F

@transaction.atomic
def transfer_money(from_user_id, to_user_id, amount):
    """Атомарный трансфер денег между счетами"""
    
    # Заблокировать обе строки (избегаем deadlock если заблокировать в одинаковом порядке)
    from_user = User.objects.select_for_update()\
        .filter(id=from_user_id).get()
    to_user = User.objects.select_for_update()\
        .filter(id=to_user_id).get()
    
    # Проверить достаточно ли средств
    if from_user.balance < amount:
        raise ValueError("Insufficient funds")
    
    # Обновить оба счета атомарно
    User.objects.filter(id=from_user_id)\
        .update(balance=F('balance') - amount)
    User.objects.filter(id=to_user_id)\
        .update(balance=F('balance') + amount)
    
    # Создать запись в истории
    Transaction.objects.create(
        from_user_id=from_user_id,
        to_user_id=to_user_id,
        amount=amount,
        status='completed'
    )
    
    # Если вот тут ошибка — всё откатится
    send_notification(to_user, f"Received {amount}")

Savepoints (точки сохранения)

from django.db import transaction

with transaction.atomic():
    user = User.objects.create(username='john')
    
    # Создать точку сохранения
    with transaction.atomic():
        try:
            profile = Profile.objects.create(user=user, invalid_data='x')
        except:
            # Откатить только profile, user остаётся
            transaction.set_rollback(True)
    
    # user всё ещё существует, profile создан не был

Явное управление коммитом

from django.db import connection, transaction

# Явный контроль
transaction.set_autocommit(False)
try:
    user = User.objects.create(username='john')
    transaction.commit()  # Явный commit
except Exception:
    transaction.rollback()  # Явный rollback
finally:
    transaction.set_autocommit(True)

# Использовать raw SQL в транзакции
with transaction.atomic():
    with connection.cursor() as cursor:
        cursor.execute(
            "UPDATE users SET balance = balance + %s WHERE id = %s",
            [100, 1]
        )

Конфигурация DATABASES

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'ATOMIC_REQUESTS': True,  # Каждый request в отдельной транзакции
        'CONN_MAX_AGE': 600,  # Переиспользовать соединение 10 минут
    }
}

Best Practices

  1. Используй transaction.atomic() для критичных операций
  2. SELECT FOR UPDATE для избегания race conditions
  3. F() выражения для атомарных обновлений
  4. Минимизируй время в транзакции (не делай HTTP запросы)
  5. Обрабатывай исключения при откатах
  6. Тестируй параллельные сценарии с threading/multiprocessing

Антипаттерны

# ❌ ПЛОХО: HTTP запрос внутри транзакции
@transaction.atomic
def create_user(data):
    user = User.objects.create(**data)
    response = requests.post('https://api.example.com/sync', json=user.as_dict())  # долго!
    return user

# ✅ ХОРОШО: сначала создать, потом синхронизировать
@transaction.atomic
def create_user(data):
    return User.objects.create(**data)

def sync_user_async(user_id):
    user = User.objects.get(id=user_id)
    requests.post('https://api.example.com/sync', json=user.as_dict())

Транзакции — это основа надёжного работы приложений. Понимание их поведения критично при разработке на Django.

Как работают транзакции в Django? | PrepBro