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

Зачем нужен on_commit в Django?

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

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

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

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

on_commit в Django: для чего нужен

on_commit — это встроенный в Django механизм для выполнения код после успешного завершения транзакции БД. Это критически важная функция для обеспечения консистентности данных и надежности асинхронных операций.

Основная проблема, которую решает on_commit

Представьте, что вы создаете пользователя и отправляете ему письмо на почту:

# ❌ ПЛОХО: может привести к ошибкам
from django.core.mail import send_mail
from .models import User

def create_user(email, password):
    user = User.objects.create_user(email=email, password=password)
    send_mail(
        'Welcome!',
        'Thanks for signing up',
        'from@example.com',
        [email]
    )
    return user

Почему это плохо:

  1. Если письмо отправляется, но транзакция откатится — пользователь не будет в БД, но письмо уже отправлено
  2. Отправка почты может быть медленной — блокирует ответ HTTP, плохо для UX
  3. Если отправка письма упадет — вся операция создания пользователя откатится

Решение: on_commit callback

from django.db import transaction
from django.core.mail import send_mail
from .models import User

def create_user(email, password):
    def send_welcome_email():
        send_mail(
            'Welcome!',
            'Thanks for signing up',
            'from@example.com',
            [email]
        )
    
    user = User.objects.create_user(email=email, password=password)
    # Этот callback выполнится ТОЛЬКО если транзакция успешно завершится
    transaction.on_commit(send_welcome_email)
    return user

Теперь:

  1. Пользователь создается в БД
  2. Только если это успешно — выполняется callback
  3. Письмо отправляется ПОСЛЕ успешного коммита
  4. Никогда не отправим письмо без пользователя в БД

Как это работает

from django.db import transaction

def process_order(order_id):
    order = Order.objects.get(id=order_id)
    order.status = 'processing'
    order.save()
    
    # Регистрируем callback, который выполнится ПОСЛЕ save()
    transaction.on_commit(lambda: send_order_notification(order_id))
    
    return order

Порядок выполнения:

1. На_commit() вызывается (регистрирует callback)
2. order.save() выполняется (инсерт в БД)
3. Транзакция завершается (COMMIT)
4. ✅ Callback выполняется

Если произойдет ошибка на шаге 2 или 3 — callback никогда не выполнится.

Практические примеры

Пример 1: Отправка email после создания пользователя

from django.db import transaction
from django.core.mail import send_mail
from .models import User

def register_user(email, name):
    user = User.objects.create_user(email=email, username=name)
    
    transaction.on_commit(
        lambda: send_mail(
            'Welcome',
            f'Hi {name}',
            'noreply@example.com',
            [email]
        )
    )
    
    return user

Пример 2: Запуск задачи Celery после изменения статуса

from django.db import transaction
from .tasks import process_payment
from .models import Order

def approve_order(order_id):
    order = Order.objects.get(id=order_id)
    order.status = 'approved'
    order.save()
    
    # Запустить обработку только если заказ успешно сохранен
    transaction.on_commit(
        lambda: process_payment.delay(order_id)
    )

Пример 3: Инвалидация кеша

from django.db import transaction
from django.core.cache import cache
from .models import Product

def update_product(product_id, new_price):
    product = Product.objects.get(id=product_id)
    product.price = new_price
    product.save()
    
    # Очистить кеш только после успешного сохранения
    transaction.on_commit(
        lambda: cache.delete(f'product:{product_id}')
    )

Nested transactions и on_commit

Важно понимать, что on_commit срабатывает только на самом внешнем уровне транзакции:

from django.db import transaction

with transaction.atomic():
    # Outer transaction
    user = User.objects.create(name='Alice')
    transaction.on_commit(lambda: print("Outer commit"))
    
    with transaction.atomic():
        # Nested transaction (savepoint)
        user.email = 'alice@example.com'
        user.save()
        transaction.on_commit(lambda: print("Inner commit"))  # не сработает!

# Вывод:
# Outer commit
# Inner commit (если это не savepoint)

Если это savepoint (не SERIALIZABLE), callback из вложенной транзакции не выполнится.

on_commit с несколькими callbacks

Можно регистрировать несколько callbacks:

from django.db import transaction

def create_user_with_profile(email, name):
    user = User.objects.create_user(email=email, username=name)
    Profile.objects.create(user=user, bio='New member')
    
    # Несколько callbacks
    transaction.on_commit(lambda: send_welcome_email(email))
    transaction.on_commit(lambda: log_user_creation(user.id))
    transaction.on_commit(lambda: update_analytics(user.id))
    
    return user

Все будут выполнены в порядке регистрации после успешного коммита.

Антипаттерны: что НЕЛЬЗЯ делать

# ❌ ПЛОХО: запросы к БД в callback могут упасть
def bad_callback():
    user = User.objects.get(id=user_id)  # может быть ошибка
    user.last_seen = now()
    user.save()

transaction.on_commit(bad_callback)

# ✅ ХОРОШО: используй на_commit для побочных эффектов, не БД логики
def good_callback():
    send_slack_notification(f"User {user_id} created")
    log_event('user_created', user_id)

transaction.on_commit(good_callback)

Сравнение: с on_commit и без

СценарийБез on_commitС on_commit
Письмо отправляется, но save() откатитсяПисьмо отправлено, пользователя нет (BUG)Письмо не отправлено (OK)
Блокирует HTTP responseДа, если письмо медленноеНет, запускается после ответа
Консистентность данныхНарушенаГарантирована
Сложные операцииМожет быть проблемноРаботает надежно

Реальный сценарий: создание заказа с уведомлением

from django.db import transaction
from django.core.mail import send_mail
from .models import Order, OrderItem
from .tasks import send_slack_notification

@transaction.atomic  # Весь метод в одной транзакции
def create_order(user, items_data):
    order = Order.objects.create(
        user=user,
        status='pending',
        total=sum(item['price'] * item['qty'] for item in items_data)
    )
    
    for item_data in items_data:
        OrderItem.objects.create(
            order=order,
            product_id=item_data['product_id'],
            quantity=item_data['qty']
        )
    
    # Callback выполнится ТОЛЬКО если весь блок атомик выполнился успешно
    transaction.on_commit(
        lambda: send_slack_notification.delay(f"New order {order.id}")
    )
    
    return order

Вывод

on_commit в Django — это essential pattern для:

  • Обеспечения консистентности данных
  • Запуска асинхронных задач (Celery)
  • Отправки уведомлений (email, SMS, push)
  • Инвалидации кеша
  • Ведения логов аудита

Без него вы рискуете иметь расхождения между БД и внешними системами. Это одна из самых важных фич Django для production-grade приложений.