💻 Примеры — Урок 36

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

⚡ Три примера урока

  • Пример 1: модель Book с мягким удалением + SoftDeleteManager
  • Пример 2: оптимизация ORM — select_related + prefetch_related
  • Пример 3: создание Publisher + Book в транзакции с on_commit

Пример 1: Мягкое удаление модели Book

Полная реализация soft deletion: менеджер, модель, API-эндпоинты, тестирование через Postman.

managers.py

from django.db import models

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

models.py

from django.db import models
from .managers import SoftDeleteManager

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    published_date = models.DateField()
    publisher = models.ForeignKey('Publisher', on_delete=models.CASCADE,
                                  null=True, blank=True)
    created_at = models.DateTimeField(null=True, blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2,
                                null=True, blank=True)
    discounted_price = models.DecimalField(max_digits=10, decimal_places=2,
                                           null=True, blank=True)
    is_bestseller = models.BooleanField(default=False)
    genres = models.ManyToManyField('Genre', related_name='books')
    is_banned = models.BooleanField(default=False)
    is_deleted = models.BooleanField(default=False)  # soft deletion

    objects = SoftDeleteManager()       # только не удалённые
    all_objects = models.Manager()      # все записи

    def delete(self, *args, **kwargs):
        self.is_deleted = True
        self.save()

    def __str__(self):
        return self.title

Миграция

python manage.py makemigrations
python manage.py migrate

Тестирование в Django shell

python manage.py shell

from books.models import Book

# Создаём книгу
book = Book.objects.get(pk=1)

# Мягкое удаление
book.delete()
# SQL: UPDATE books_book SET is_deleted=True WHERE id=1
# Физического DELETE нет!

# Теперь книга НЕ видна через стандартный менеджер
Book.objects.filter(pk=1).exists()   # False

# Но данные в базе есть — через all_objects
Book.all_objects.filter(pk=1).exists()  # True
Book.all_objects.get(pk=1).is_deleted   # True

Пример 2: Оптимизация ленивой загрузки

Проблема: N+1 запросов

python manage.py shell

# Плохо: N+1 запросов
books = Book.objects.all()      # 1 SELECT
for book in books:
    print(book.publisher)       # +1 SELECT для каждой книги
    for genre in book.genres.all():  # ещё +N SELECT
        print(genre)

Решение: select_related + prefetch_related

# Хорошо: 3 запроса вместо 1 + N + N*M
books = Book.objects.select_related('publisher').prefetch_related('genres').all()
# SQL 1: SELECT books JOIN publishers
# SQL 2: SELECT genres WHERE book_id IN (...)

for book in books:
    print("Publisher:", book.publisher)   # из кеша JOIN
    for genre in book.genres.all():       # из кеша prefetch
        print("  Genre:", genre)

views.py с оптимизацией

from rest_framework.generics import ListAPIView
from .models import Book
from .serializers import BookSerializer

class BookListView(ListAPIView):
    queryset = Book.objects.select_related('publisher').prefetch_related('genres')
    serializer_class = BookSerializer

Пример 3: Создание Publisher + Book в транзакции

urls.py

from django.urls import path
from .views import create_book_with_publisher

urlpatterns = [
    path('books/transactions/', create_book_with_publisher),
]

views.py — transaction.atomic + on_commit

from django.db import transaction
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Book, Publisher
from .serializers import BookSerializer

@api_view(['POST'])
def create_book_with_publisher(request):
    def notify_success(book):
        # В реальном проекте: отправка email, задача Celery и т.д.
        print(f"\n{'—' * 25} Book '{book.title}' created! {'—' * 25}\n")

    try:
        with transaction.atomic():
            publisher = Publisher.objects.create(
                name=request.data.get('publisher_name', 'Default Publisher'),
                established_date="2024-06-01"
            )
            book = Book.objects.create(
                title=request.data.get('title', 'New Book'),
                author=request.data.get('author', 'Unknown Author'),
                published_date="2024-06-02",
                publisher=publisher
            )
            # on_commit: выполнится только если транзакция завершилась успешно
            transaction.on_commit(lambda: notify_success(book))
            serializer = BookSerializer(book)
        return Response(serializer.data, status=201)
    except Exception as e:
        # Транзакция уже откатилась — Publisher и Book не созданы
        return Response({'error': str(e)}, status=400)

Тест через Postman

POST http://127.0.0.1:8000/books/transactions/
Content-Type: application/json

{
    "title": "Django for Professionals",
    "author": "William S. Vincent",
    "publisher_name": "WelcomeToCode Press"
}

# Ответ 201:
{
    "id": 42,
    "title": "Django for Professionals",
    "author": "William S. Vincent",
    "published_date": "2024-06-02",
    "publisher": 7,
    "is_deleted": false
}

Пример с откатом — set_rollback

@api_view(['POST'])
def create_unique_publisher_book(request):
    try:
        with transaction.atomic():
            publisher = Publisher.objects.create(name="New Publisher",
                                                 established_date="2024-06-01")
            book = Book.objects.create(title="New Book", author="Author",
                                       published_date="2024-06-02",
                                       publisher=publisher)
            # Откатить если Publisher с таким именем уже был
            if Publisher.objects.filter(name="New Publisher").count() > 1:
                transaction.set_rollback(True)
            serializer = BookSerializer(book)
        return Response(serializer.data)
    except Exception as e:
        return Response({'error': str(e)}, status=400)