📖 Теория — Урок 29

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

⚡ Ключевые концепции урока

  • StringRelatedField — сериализует через __str__(), только чтение
  • SlugRelatedField — читает/пишет по slug-полю; нужен queryset=
  • PrimaryKeyRelatedField — читает/пишет по ID; many=True для M2M
  • APIView — базовый CBV, методы get/post/put/patch/delete
  • Generic Views — готовые классы ListCreateAPIView, RetrieveUpdateDestroyAPIView
  • ModelViewSet — полный CRUD одним классом; Router строит URLs автоматически
  • as_view() — превращает класс в функцию для urls.py

Часть 1: Поля для отношений между моделями

В DRF связанные объекты (ForeignKey, ManyToManyField) по умолчанию сериализуются по первичному ключу. Но DRF предоставляет несколько полей для гибкой настройки представления связей.

4. Many-to-Many с PrimaryKeyRelatedField

Для ManyToManyField используется тот же PrimaryKeyRelatedField, но с параметром many=True.

Модели

# models.py
class Genre(models.Model):
    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name

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')  # M2M

Сериализатор с M2M

# serializers.py
from rest_framework import serializers
from .models import Book, Genre, Publisher

class BookSerializer(serializers.ModelSerializer):
    publisher = serializers.PrimaryKeyRelatedField(
        queryset=Publisher.objects.all()
    )
    genres = serializers.PrimaryKeyRelatedField(
        queryset=Genre.objects.all(),
        many=True  # указывает на список объектов
    )

    class Meta:
        model = Book
        fields = '__all__'

Сериализатор для Genre

# serializers.py
from library.models import Genre

class GenreSerializer(serializers.ModelSerializer):
    class Meta:
        model = Genre
        fields = '__all__'

Пример PUT-запроса с жанрами

PUT http://127.0.0.1:8000/books/27/

{
    "publisher": 3,
    "title": "Django for Beginners",
    "author": "William Vincent",
    "published_date": "2021-05-21",
    "created_at": null,
    "price": "39.99",
    "discounted_price": null,
    "is_bestseller": true,
    "genres": [1, 2]  # ID существующих жанров
}

# Ожидаемый ответ:
{
    "id": 27,
    "title": "Django For Beginners",
    "author": "William Vincent",
    "published_date": "2021-05-21",
    "created_at": null,
    "price": "39.99",
    "discounted_price": null,
    "is_bestseller": true,
    "publisher": 3,
    "genres": [1, 2]
}
ПолеПредставлениеЗаписьПрименение
StringRelatedField__str__() объектаНет (read-only)Читаемый вывод
SlugRelatedFieldЗначение slug-поляПо slugЧеловекочитаемые URL
PrimaryKeyRelatedFieldID (int)По IDFK и M2M (many=True)

Часть 2: Class-Based Views

Class-Based Views (CBV) — подход к созданию представлений в DRF, где представления реализуются как классы. CBV обеспечивают объектно-ориентированный подход, позволяя лучше структурировать, расширять и переиспользовать код.

CBV делятся на несколько категорий — от базовых до высокоуровневых. Каждая категория предоставляет различный уровень абстракции.

5. Иерархия Class-Based Views

Уровень 1: APIView (базовый)

Основной класс для создания CBV. Предоставляет методы для обработки HTTP-запросов: get, post, put, patch, delete.

  • Использование: создание кастомных представлений с нуля, требующих специфической логики
  • Уровень: Низкоуровневый — всё пишется вручную

Уровень 2: GenericAPIView + Mixins (средний)

GenericAPIView расширяет APIView и добавляет базовую функциональность: пагинация, фильтрация, атрибуты queryset и serializer_class.

Миксины — вспомогательные классы, добавляющие CRUD-функциональность:

  • CreateModelMixin — метод create() для создания объекта
  • RetrieveModelMixin — метод retrieve() для получения объекта
  • UpdateModelMixin — методы update() и partial_update()
  • DestroyModelMixin — метод destroy() для удаления
  • ListModelMixin — метод list() для списка объектов

Комбинация GenericAPIView + нужных миксинов = гибкие представления с минимальным кодом.

Уровень 3: Generic Views (высокоуровневый)

Готовые классы, объединяющие GenericAPIView и нужные миксины:

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

Уровень 4: ViewSets (максимальный уровень абстракции)

ModelViewSet — полный набор операций CRUD. Объединяет все generic представления и миксины. Используется совместно с Router для автоматической генерации URL.

from rest_framework.viewsets import ModelViewSet
from rest_framework.routers import DefaultRouter

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# Router автоматически создаёт все URL
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = router.urls
# Результат: /books/, /books/{pk}/ с GET/POST/PUT/PATCH/DELETE

ReadOnlyModelViewSet — только операции чтения (list и retrieve). Для представлений, где запись через API не требуется.

6. Преимущества CBV перед FBV

АспектFBV (Function-Based)CBV (Class-Based)
Структура кодаПроцедурный подходОбъектно-ориентированный, классы
Повторное использованиеКопирование кодаНаследование, миксины
РасширениеМодификация функцииПереопределение методов в подклассе
Стандартные операцииПишутся вручнуюGeneric Views готовы "из коробки"
ТестированиеВся функция целикомКаждый метод отдельно

Часть 3: APIView

7. Что такое APIView

APIView — базовый класс представления в Django REST Framework. Расширяет стандартный класс Django View и предоставляет функциональность для работы с DRF-запросами и ответами.

Основные возможности APIView

  • Обработка HTTP-запросов: методы get, post, put, patch, delete
  • Встроенная поддержка API: автоматическая обработка запросов и ответов в формате JSON
  • Кастомизация: переопределение любого метода для специфической логики

Представление для списка и создания объектов

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Book
from .serializers import BookSerializer

class BookListCreateView(APIView):
    def get(self, request):
        """GET /books/ — список всех книг"""
        books = Book.objects.all()
        serializer = BookSerializer(books, many=True)
        return Response(serializer.data)

    def post(self, request):
        """POST /books/ — создание новой книги"""
        serializer = BookSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Объяснения: get обрабатывает GET-запросы — возвращает список всех объектов. post обрабатывает POST-запросы — создаёт новый объект.

Представление для получения, обновления и удаления объектов

# views.py
class BookDetailUpdateDeleteView(APIView):
    def get(self, request, pk):
        """GET /books/{pk}/ — получение конкретной книги"""
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return Response({'error': 'Book not found'}, status=status.HTTP_404_NOT_FOUND)
        serializer = BookSerializer(book)
        return Response(serializer.data)

    def put(self, request, pk):
        """PUT /books/{pk}/ — полное обновление"""
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return Response({'error': 'Book not found'}, status=status.HTTP_404_NOT_FOUND)
        serializer = BookSerializer(book, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        """DELETE /books/{pk}/ — удаление"""
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return Response({'error': 'Book not found'}, status=status.HTTP_404_NOT_FOUND)
        book.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Маршрутизация для APIView

# urls.py
from django.urls import path
from .views import BookListCreateView, BookDetailUpdateDeleteView

urlpatterns = [
    path('books/', BookListCreateView.as_view(), name='book-list-create'),
    path('books/<int:pk>/', BookDetailUpdateDeleteView.as_view(), name='book-detail-update-delete'),
]

Объяснение: BookListCreateView.as_view() преобразует класс представления в функцию для маршрутизации. <int:pk> захватывает идентификатор из URL.

Ключевое правило: при регистрации CBV в urls.py всегда вызывайте .as_view(). Без него Django не сможет вызвать класс как view-функцию.