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

🎯 Уроки 33–38 К оглавлению урока

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

  • AttributeError: queryset not set — забыли указать queryset или get_queryset() в Generic View / ViewSet.
  • N+1 запросов — доступ к связанным объектам без select_related/prefetch_related.
  • Soft Delete не работает — не переопределили SoftDeleteManager или используете Model._default_manager.

1. queryset не задан

Ошибка: AssertionError: 'BookListView' should either include a queryset attribute, or override the get_queryset() method.

Причина: Класс Generic View / ViewSet не имеет ни атрибута queryset, ни метода get_queryset().

# Неверно
class BookListView(ListAPIView):
    serializer_class = BookSerializer
    # queryset не указан!

# Верно — атрибут
class BookListView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# Верно — метод
class BookListView(ListAPIView):
    serializer_class = BookSerializer
    def get_queryset(self):
        return Book.objects.all()

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

Симптом: Запрос работает, но при 100 объектах выполняется 101 SQL-запрос. Логирование показывает повторяющиеся SELECT.

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

# Верно — select_related для FK
books = Book.objects.select_related('publisher').all()

# Верно — prefetch_related для M2M
books = Book.objects.prefetch_related('genres').all()

3. Soft Delete — записи всё равно видны

Симптом: После вызова obj.delete() объект всё равно возвращается в API.

# Неверно — нет SoftDeleteManager
class Category(models.Model):
    is_deleted = models.BooleanField(default=False)
    def delete(self): self.is_deleted = True; self.save()
# Category.objects.all() вернёт и удалённые!

# Верно — менеджер фильтрует автоматически
class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

class Category(models.Model):
    is_deleted = models.BooleanField(default=False)
    objects = SoftDeleteManager()
    def delete(self): self.is_deleted = True; self.save()

4. @action не доступен в URL

Ошибка: GET /genres/statistic/ возвращает 404.

# Неверно — ViewSet не подключён через Router
urlpatterns = [
    path('genres/', GenreViewSet.as_view({'get': 'list'})),
    # @action не будет зарегистрирован!
]

# Верно — используйте Router
router = DefaultRouter()
router.register(r'genres', GenreViewSet)
urlpatterns = [path('', include(router.urls))]
# GET /genres/statistic/ теперь работает

5. Пагинация не возвращает метаданные

Симптом: Ответ API — просто список объектов, без count, next, previous.

# Неверно — pagination_class не указан
class BookListView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    # нет pagination_class

# Верно — указать класс пагинации
class BookListView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = PageNumberPagination

# Или глобально в settings.py:
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

6. Транзакция не откатывается при ошибке

Симптом: При ошибке во второй операции первая операция сохраняется в БД (частичные данные).

# Неверно — нет атомарности
def create_task(request):
    category = Category.objects.create(name='Work')
    task = Task.objects.create(category=category, title='')  # ошибка!
    # category уже сохранена в БД!

# Верно — transaction.atomic откатит оба
def create_task(request):
    with transaction.atomic():
        category = Category.objects.create(name='Work')
        task = Task.objects.create(category=category, title='')
        # если Task.create() бросит исключение — Category тоже откатится

7. Логирование SQL не работает

Симптом: LOGGING настроен, но SQL-запросы не выводятся.

# Частая причина 1: DEBUG = False
# SQL-логирование работает только при DEBUG = True
DEBUG = True  # убедитесь, что это True в dev-окружении

# Частая причина 2: неверный ключ логгера
# Неверно
'loggers': {'django.db': {'level': 'DEBUG'}}  # неполный ключ

# Верно
'loggers': {'django.db.backends': {'level': 'DEBUG'}}

8. DjangoFilterBackend не подключён

Ошибка: ImproperlyConfigured: Specified DEFAULT_FILTER_BACKENDS is not available или фильтрация по полям не работает.

# Неверно — пакет не установлен / не добавлен в INSTALLED_APPS
filter_backends = [DjangoFilterBackend]  # ImportError

# Верно
# 1. Установить: pip install django-filter
# 2. settings.py
INSTALLED_APPS = ['django_filters', ...]
# 3. Импорт
from django_filters.rest_framework import DjangoFilterBackend

9. CursorPagination — ошибка поля для cursor

Ошибка: CursorPagination requires a 'created' field in the queryset ordering.

# Неверно — поле 'created' не существует в модели
class MyCursorPagination(CursorPagination):
    ordering = 'created'  # по умолчанию CursorPagination ищет 'created'

# Верно — явно указать существующее поле
class MyCursorPagination(CursorPagination):
    page_size = 10
    ordering = '-created_at'  # поле, которое реально есть в модели