🐛 Типичные ошибки — Урок 29

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

⚡ Топ-7 ошибок урока

  • StringRelatedField для записи — он read-only, используй SlugRelatedField или PrimaryKeyRelatedField
  • Нет queryset= в SlugRelatedField/PrimaryKeyRelatedField — поле не может валидировать входные данные
  • Нет many=True для M2M поля — сериализатор ожидает объект, а не список
  • Нет .as_view() в urls.py для CBV — Django не умеет вызывать класс как view
  • Нет partial=True при PATCH — все поля становятся обязательными
  • N+1 проблема — нет select_related/prefetch_related при сериализации связей
  • Response(status=201) вместо Response(status=status.HTTP_201_CREATED) — магические числа

1. StringRelatedField при попытке записи

Ошибка: Использование StringRelatedField для поля, в которое нужно записывать данные.
# НЕПРАВИЛЬНО — StringRelatedField только для чтения
class BookSerializer(serializers.ModelSerializer):
    publisher = serializers.StringRelatedField()
    # POST с {"publisher": "awesome-publisher"} → ОШИБКА
    # StringRelatedField не поддерживает запись
# ПРАВИЛЬНО — SlugRelatedField для чтения/записи по slug
class BookSerializer(serializers.ModelSerializer):
    publisher = serializers.SlugRelatedField(
        slug_field='slug',
        queryset=Publisher.objects.all()
    )
    # POST с {"publisher": "awesome-publisher"} — работает

Правило: StringRelatedField = только GET. Для POST/PUT/PATCH используйте SlugRelatedField или PrimaryKeyRelatedField.

2. Отсутствие queryset= в related fields

Ошибка: AssertionError: Relational field must provide a 'queryset' argument
# НЕПРАВИЛЬНО
publisher = serializers.PrimaryKeyRelatedField()
# или
publisher = serializers.SlugRelatedField(slug_field='slug')

# ПРАВИЛЬНО
publisher = serializers.PrimaryKeyRelatedField(
    queryset=Publisher.objects.all()  # обязателен для записи
)
publisher = serializers.SlugRelatedField(
    slug_field='slug',
    queryset=Publisher.objects.all()  # или read_only=True
)

Исключение: если поле только для чтения — используйте read_only=True вместо queryset=.

3. Нет many=True для ManyToManyField

Ошибка: Сериализатор ожидает один объект, а не список.
# НЕПРАВИЛЬНО — genres — это M2M, но many=True не указан
genres = serializers.PrimaryKeyRelatedField(
    queryset=Genre.objects.all()
)
# POST с {"genres": [1, 2]} → ошибка или только первый жанр

# ПРАВИЛЬНО
genres = serializers.PrimaryKeyRelatedField(
    queryset=Genre.objects.all(),
    many=True  # обязателен для ManyToManyField
)

4. Забыли .as_view() в urls.py

Ошибка: TypeError: view must be a callable or a list/tuple in the case of include()
# НЕПРАВИЛЬНО — класс не является view-функцией
urlpatterns = [
    path('books/', BookListCreateView, name='books'),  # TypeError!
]

# ПРАВИЛЬНО
urlpatterns = [
    path('books/', BookListCreateView.as_view(), name='books'),
]

5. PUT без partial=True принимает неполные данные

Ошибка: PATCH-запрос с одним полем падает с 400 Bad Request.
# НЕПРАВИЛЬНО — PATCH без partial=True требует все поля
def patch(self, request, pk):
    book = self.get_object(pk)
    serializer = BookSerializer(book, data=request.data)
    # PATCH {"title": "New Title"} → ошибка: author is required

# ПРАВИЛЬНО
def patch(self, request, pk):
    book = self.get_object(pk)
    serializer = BookSerializer(book, data=request.data, partial=True)
    # partial=True: только указанные поля обновляются

6. N+1 проблема при сериализации связей

Проблема: Для каждой книги в списке выполняется отдельный запрос к Publisher и каждому Genre.
# НЕПРАВИЛЬНО — N+1 запросов для 100 книг = 100+ запросов
def get(self, request):
    books = Book.objects.all()  # 1 запрос
    # Для каждой книги: book.publisher (ещё 1 запрос) + book.genres (ещё 1 запрос)
    serializer = BookSerializer(books, many=True)
    return Response(serializer.data)

# ПРАВИЛЬНО — 3 запроса независимо от количества книг
def get(self, request):
    books = Book.objects.select_related('publisher')\
                        .prefetch_related('genres')\
                        .all()
    serializer = BookSerializer(books, many=True)
    return Response(serializer.data)

7. Магические числа вместо статус-констант

Проблема: Код с числами 200, 201, 404 трудно читать и легко ошибиться.
# НЕПРАВИЛЬНО — магические числа
return Response(data, status=201)
return Response({'error': '...'}, status=404)

# ПРАВИЛЬНО — читаемые константы
from rest_framework import status
return Response(data, status=status.HTTP_201_CREATED)
return Response({'error': '...'}, status=status.HTTP_404_NOT_FOUND)

8. Дублирование try/except в каждом методе

Проблема: Нарушение принципа DRY — одинаковый код в get, put, delete.
# НЕПРАВИЛЬНО — дублирование
class BookDetailView(APIView):
    def get(self, request, pk):
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return Response({'error': 'Not found'}, status=404)
        ...
    def put(self, request, pk):
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return Response({'error': 'Not found'}, status=404)
        ...

# ПРАВИЛЬНО — вынести в get_object()
class BookDetailView(APIView):
    def get_object(self, pk):
        try:
            return Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return None

    def get(self, request, pk):
        book = self.get_object(pk)
        if book is None:
            return Response({'error': 'Not found'}, status=status.HTTP_404_NOT_FOUND)
        ...