📖 Теория: конспект DRF-блока (Уроки 33–38)

🎯 Итоговое повторение К оглавлению урока

⚡ Краткий конспект

  • GenericAPIView — queryset + serializer_class; get_queryset() для динамической фильтрации; get_object() для поиска одного объекта; get_serializer_class() для разных сериализаторов.
  • Generic Views — 9 готовых классов: ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView и их комбинации.
  • lookup_field — поле модели для поиска (по умолчанию pk); lookup_url_kwarg — имя параметра в URL.
  • ViewSets — ModelViewSet (полный CRUD), ReadOnlyModelViewSet (только чтение), GenericViewSet + миксины (кастомный набор). Кастомные методы через @action.
  • Router — SimpleRouter или DefaultRouter; router.register(r'prefix', ViewSet).
  • filter_backends — DjangoFilterBackend (filterset_fields), SearchFilter (search_fields), OrderingFilter (ordering_fields).
  • Soft Deletion — поле is_deleted; переопределение delete() для установки флага; SoftDeleteManager фильтрует is_deleted=False.
  • N+1 проблема — select_related() для FK/OneToOne; prefetch_related() для ManyToMany.
  • Транзакции — @transaction.atomic / with transaction.atomic(); on_commit; set_rollback.
  • Пагинация — PageNumberPagination (?page=N), LimitOffsetPagination (?limit=N&offset=N), CursorPagination (стабильная). Глобальная через DEFAULT_PAGINATION_CLASS.
  • Логирование SQL — LOGGING с 'django.db.backends' уровня DEBUG.

1. GenericAPIView (Урок 33)

GenericAPIView расширяет APIView, добавляя поддержку queryset, сериализаторов, фильтрации и пагинации. Разработчики обычно используют его через готовые Generic Views, а не напрямую.

Ключевые атрибуты

АтрибутНазначение
querysetНабор данных представления
serializer_classКласс сериализатора
filter_backendsСписок классов фильтрации
pagination_classКласс пагинации
lookup_fieldПоле модели для поиска (default: pk)
lookup_url_kwargПараметр URL для lookup_field

Ключевые методы

class BookListView(GenericAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def get_queryset(self):
        # Динамическая фильтрация по параметру запроса
        queryset = Book.objects.all()
        author = self.request.query_params.get('author')
        if author:
            queryset = queryset.filter(author=author)
        return queryset

    def get_serializer_class(self):
        # Разные сериализаторы для разных методов
        if self.request.method == 'POST':
            return BookCreateSerializer
        return BookSerializer

    def get_serializer_context(self):
        # Передача дополнительного контекста в сериализатор
        context = super().get_serializer_context()
        context['include_related'] = self.request.query_params.get(
            'include_related', 'false'
        ).lower() == 'true'
        return context

Переопределение get_object()

class BookDetailView(RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def get_object(self):
        pk = self.kwargs.get('pk')
        try:
            book = self.queryset.get(pk=pk, is_banned=False)
        except Book.DoesNotExist:
            raise NotFound(detail=f"Book with id '{pk}' not found or is banned.")
        return book

2. Generic Views (Уроки 33–34)

Готовые классы на основе GenericAPIView + миксины. Каждый реализует стандартные CRUD-операции.

КлассHTTP-методыНазначение
ListAPIViewGETСписок объектов
CreateAPIViewPOSTСоздание
RetrieveAPIViewGETОдин объект по pk
UpdateAPIViewPUT/PATCHОбновление
DestroyAPIViewDELETEУдаление
ListCreateAPIViewGET, POSTСписок + создание
RetrieveUpdateAPIViewGET, PUT, PATCHЧтение + обновление
RetrieveDestroyAPIViewGET, DELETEЧтение + удаление
RetrieveUpdateDestroyAPIViewGET, PUT, PATCH, DELETEВсе три операции над одним объектом

Минимальное использование

from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
from .models import Book
from .serializers import BookSerializer

class BookListCreateView(ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

class BookDetailView(RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

Переопределение методов миксинов

class BookListCreateView(ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def create(self, request, *args, **kwargs):
        data = request.data.copy()
        if not data.get('author'):
            data['author'] = 'Unknown Author'
        serializer = self.get_serializer(data=data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

lookup_field и lookup_url_kwarg

class GenreDetailView(RetrieveUpdateDestroyAPIView):
    queryset = Genre.objects.all()
    serializer_class = GenreSerializer
    lookup_field = 'name'          # поиск по полю 'name', не pk
    lookup_url_kwarg = 'genre_name'  # имя параметра в URL

# urls.py
path('genres/<str:genre_name>/', GenreDetailView.as_view())

3. ViewSets и Router (Урок 35)

ViewSet объединяет логику нескольких HTTP-методов в одном классе. Router автоматически создаёт URL-маршруты.

Виды ViewSets

КлассДоступные действия
ModelViewSetlist, create, retrieve, update, partial_update, destroy
ReadOnlyModelViewSetlist, retrieve
GenericViewSetнет действий по умолчанию — добавляются миксинами
from rest_framework import viewsets
from .models import Genre
from .serializers import GenreSerializer

class GenreViewSet(viewsets.ModelViewSet):
    queryset = Genre.objects.all()
    serializer_class = GenreSerializer

Кастомные методы через @action

from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Count

class GenreViewSet(viewsets.ModelViewSet):
    queryset = Genre.objects.all()
    serializer_class = GenreSerializer

    @action(detail=False, methods=['get'])
    def statistic(self, request):
        genres = Genre.objects.annotate(book_count=Count('books'))
        data = [
            {'id': g.id, 'genre': g.name, 'book_count': g.book_count}
            for g in genres
        ]
        return Response(data)
    # GET /genres/statistic/

Router: SimpleRouter vs DefaultRouter

from rest_framework.routers import DefaultRouter
from .views import GenreViewSet

router = DefaultRouter()  # добавляет страницу /api/ с обзором всех маршрутов
# SimpleRouter не добавляет страницу корня API

router.register(r'genres', GenreViewSet)

urlpatterns = [
    path('', include(router.urls)),
]
# Генерирует: GET/POST /genres/, GET/PUT/PATCH/DELETE /genres/{pk}/

GenericViewSet + миксины (кастомный набор)

from rest_framework import mixins, viewsets

class GenreListRetrieveUpdateViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet
):
    queryset = Genre.objects.all()
    serializer_class = GenreSerializer
    # Только list, retrieve, update — без create и destroy

4. filter_backends (Урок 37)

# settings.py
INSTALLED_APPS = ['django_filters', ...]

# views.py
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend

class BookListView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['author', 'publisher', 'is_bestseller']
    search_fields = ['title', 'author']
    ordering_fields = ['published_date', 'price']

# Примеры запросов:
# GET /books/?author=Fitzgerald
# GET /books/?search=Gatsby
# GET /books/?ordering=-price

5. Soft Deletion (Урок 37)

Записи не удаляются физически — они помечаются флагом is_deleted.

# 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 .managers import SoftDeleteManager

class Category(models.Model):
    name = models.CharField(max_length=100)
    is_deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)

    objects = SoftDeleteManager()

    def delete(self, *args, **kwargs):
        from django.utils import timezone
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save()

6. Ленивая загрузка и N+1 (Урок 37)

QuerySet — ленивый: запрос выполняется только при обращении к данным. Основная проблема — N+1 запросов при доступе к связанным объектам.

# Проблема N+1 — для каждой книги выполняется отдельный запрос к publisher
books = Book.objects.all()
for book in books:
    print(book.publisher)  # N запросов!

# Решение: select_related (JOIN) для FK/OneToOne
books = Book.objects.select_related('publisher').all()

# prefetch_related для ManyToMany и обратных FK
books = Book.objects.prefetch_related('genres').all()

# Вместе
books = Book.objects.select_related('publisher').prefetch_related('genres').all()

7. Транзакции (Урок 37)

from django.db import transaction

# Декоратор
@transaction.atomic
def my_view(request):
    ...

# Контекстный менеджер
with transaction.atomic():
    publisher = Publisher.objects.create(...)
    book = Book.objects.create(publisher=publisher, ...)

# Откат транзакции вручную
with transaction.atomic():
    ...
    if some_condition:
        transaction.set_rollback(True)

# Действие после успешного коммита
with transaction.atomic():
    book = Book.objects.create(...)
    transaction.on_commit(lambda: send_notification(book))

8. Пагинация (Урок 38)

КлассПараметры запросаОсобенности
PageNumberPagination?page=N&page_size=NНомер страницы; прост в использовании
LimitOffsetPagination?limit=N&offset=NГибкий сдвиг; привычен для SQL-разработчиков
CursorPagination?cursor=...Стабильный порядок; безопасен при частых изменениях
# Локальная настройка
from rest_framework.pagination import PageNumberPagination

class BookPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 100

class BookListView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = BookPagination

# Глобальная настройка в settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 5,
}

9. Логирование SQL-запросов (Урок 38)

# settings.py
import os

DEBUG = True  # Логирование SQL работает только при DEBUG=True

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'db.log'),
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console', 'file'],
            'level': 'DEBUG',
        },
    },
}

10. Регулярные выражения в URL (Урок 35)

from django.urls import re_path
from .views import books_by_date_view

urlpatterns = [
    re_path(
        r'^books/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/$',
        books_by_date_view,
        name='books-by-date'
    ),
]

# views.py
@api_view(['GET'])
def books_by_date_view(request, year, month, day):
    books = Book.objects.filter(
        published_date__year=year,
        published_date__month=month,
        published_date__day=day
    )
    serializer = BookSerializer(books, many=True)
    return Response({'date': f'{year}-{month}-{day}', 'books': serializer.data})