⚖️ Старый vs Новый — Урок 36

← К оглавлению урока

⚡ Ключевые изменения

  • Физическое удаление → мягкое удаление через is_deleted + менеджер
  • N+1 запросов в цикле → select_related / prefetch_related
  • Ручной set_autocommit(False)with transaction.atomic()
  • Код после операции → transaction.on_commit() для гарантий

1. Удаление записей: физическое vs мягкое

Из лекции (старое) — физическое удаление

# models.py
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    # ...

# views.py — DELETE выполняет физическое удаление
@api_view(['DELETE'])
def delete_book(request, pk):
    book = Book.objects.get(pk=pk)
    book.delete()  # SQL: DELETE FROM books WHERE id=pk
    return Response(status=204)

Запись удалена навсегда — восстановление невозможно без бекапа.

Современный подход — мягкое удаление

# managers.py
class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

# models.py
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    is_deleted = models.BooleanField(default=False)

    objects = SoftDeleteManager()

    def delete(self, *args, **kwargs):
        self.is_deleted = True
        self.save()
        # SQL: UPDATE books SET is_deleted=True WHERE id=pk

Запись остаётся в базе — можно восстановить, использовать для аудита.

2. Загрузка связанных объектов: N+1 vs оптимизация

Из лекции (старое) — N+1 запросов

# views.py
@api_view(['GET'])
def list_books(request):
    books = Book.objects.all()  # 1 запрос
    result = []
    for book in books:
        result.append({
            'title': book.title,
            'publisher': book.publisher.name,  # +1 запрос!
            'genres': [g.name for g in book.genres.all()]  # +N запросов!
        })
    return Response(result)
# Для 100 книг = 1 + 100 + 100 = 201 запрос

Критически медленно на продакшне с реальными объёмами данных.

Современный подход — оптимизация

# views.py
@api_view(['GET'])
def list_books(request):
    # 3 запроса независимо от числа книг
    books = (Book.objects
             .select_related('publisher')
             .prefetch_related('genres'))
    result = []
    for book in books:
        result.append({
            'title': book.title,
            'publisher': book.publisher.name,  # из JOIN-кеша
            'genres': [g.name for g in book.genres.all()]  # из prefetch-кеша
        })
    return Response(result)
# Для 100 книг = 3 запроса

3. Транзакции: ручной режим vs transaction.atomic

Из лекции (старое) — ручной autocommit

# views.py
@api_view(['POST'])
def create_book_and_publisher(request):
    try:
        transaction.set_autocommit(False)  # опасно: легко забыть
        publisher = Publisher.objects.create(...)
        book = Book.objects.create(publisher=publisher, ...)
        transaction.commit()
        return Response(BookSerializer(book).data)
    except Exception as e:
        transaction.rollback()
        return Response({'error': str(e)}, status=400)
    finally:
        transaction.set_autocommit(True)  # не забыть вернуть!

Сложно, легко забыть finally → утечка соединения.

Современный подход — transaction.atomic

# views.py
@api_view(['POST'])
def create_book_and_publisher(request):
    try:
        with transaction.atomic():
            publisher = Publisher.objects.create(...)
            book = Book.objects.create(publisher=publisher, ...)
            # При исключении — автоматический ROLLBACK
        return Response(BookSerializer(book).data)
    except Exception as e:
        return Response({'error': str(e)}, status=400)

Чисто, безопасно, откат происходит автоматически при любом исключении.

4. Код после операции: после сохранения vs on_commit

Из лекции (старое) — код сразу после save

# views.py
@api_view(['POST'])
def create_book(request):
    with transaction.atomic():
        book = Book.objects.create(...)
    # Уведомление отправляется немедленно
    # Но транзакция могла ещё не записаться!
    send_email_notification(book)
    return Response(...)

Если сервер перезапустился между save и email — email отправлен, книга не создана. Рассинхрон.

Современный подход — transaction.on_commit

# views.py
@api_view(['POST'])
def create_book(request):
    with transaction.atomic():
        book = Book.objects.create(...)
        # Гарантия: email только после успешного COMMIT
        transaction.on_commit(
            lambda: send_email_notification(book.id)
        )
    return Response(...)

Если транзакция откатится — callback не выполнится. Данные всегда согласованы.