🐛 Частые ошибки DRF-блока

🎯 Разбор типичных проблем К оглавлению урока

⚡ Топ-3 ошибки

  1. validated_data до is_valid() — AssertionError. Всегда вызывай is_valid() первым.
  2. Нет many=True при сериализации QuerySet — сериализатор ожидает один объект, а не список.
  3. Не добавили 'rest_framework' в INSTALLED_APPS — Browsable API не работает, статические файлы DRF не подключаются.

1. Доступ к validated_data без is_valid()

Проблема

serializer = BookSerializer(data=request.data)
# AssertionError: You must call `.is_valid()` before accessing `.validated_data`.
book = Book.objects.create(**serializer.validated_data)

Решение

serializer = BookSerializer(data=request.data)
if serializer.is_valid():
    book = serializer.save()  # вызывает create() внутри
    return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

2. Забыли many=True при сериализации QuerySet

Проблема

books = Book.objects.all()
# Ошибка — сериализует только один объект, а не список
serializer = BookSerializer(books)
return Response(serializer.data)

Решение

books = Book.objects.all()
serializer = BookSerializer(books, many=True)  # many=True для QuerySet/list
return Response(serializer.data)

3. 'rest_framework' не в INSTALLED_APPS

Проблема

Забыли добавить DRF в INSTALLED_APPS. Browsable API не отображается, статика DRF (CSS/JS) не подключается.

Решение

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',  # обязательно!
]

4. Неверный HTTP-статус при создании объекта

Проблема

@api_view(['POST'])
def book_create(request):
    serializer = BookSerializer(data=request.data)
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data)  # возвращает 200 вместо 201

Решение

return Response(serializer.data, status=status.HTTP_201_CREATED)

При создании ресурса стандарт REST требует 201 Created.

5. aggregate с annotate вместе без values()

Проблема

# Ошибка в семантике: annotate без values группирует по pk каждого объекта
author_counts = Book.objects.annotate(cnt=Count('id'))
# Выдаёт каждую книгу с cnt=1, а не количество книг каждого автора

Решение

# Сначала values() для группировки, затем annotate
author_counts = Book.objects.values('author').annotate(cnt=Count('id'))
# [{'author': 'Orwell', 'cnt': 3}, ...]

6. Нет обработки DoesNotExist в detail view

Проблема

@api_view(['GET'])
def book_detail(request, pk):
    book = Book.objects.get(pk=pk)  # Book.DoesNotExist -> 500 Internal Server Error
    serializer = BookSerializer(book)
    return Response(serializer.data)

Решение

@api_view(['GET'])
def book_detail(request, 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)

Generic views делают это автоматически через get_object_or_404.

7. Несохранённые изменения: save() забыт после update()

Проблема

@api_view(['PUT'])
def book_update(request, pk):
    book = Book.objects.get(pk=pk)
    serializer = BookSerializer(book, data=request.data)
    if serializer.is_valid():
        # Забыли serializer.save() !
        return Response(serializer.data)  # данные не сохранены в БД

Решение

if serializer.is_valid():
    serializer.save()  # обязательно!
    return Response(serializer.data)

8. Срез QuerySet после filter применяется в Python, не в SQL

Проблема

# Получить 10 последних книг автора — МЕДЛЕННО
books = list(Book.objects.filter(author="Orwell"))
last_10 = books[-10:]  # сначала грузит все книги в память!

Решение

# SQL-уровень — эффективно
last_10 = Book.objects.filter(author="Orwell").order_by('-id')[:10]