Пагинация, фильтрация, throttling

Как отдавать большие списки порциями, давать клиенту фильтровать и искать, и не дать заспамить API.

Пагинация режет большой список на страницы; фильтрация, поиск и сортировка позволяют клиенту сузить и упорядочить выдачу; throttling ограничивает число запросов за период. Вместе они превращают «голый» список в управляемый и защищённый эндпоинт.

Зачем это знать на практике

Эндпоинт, который отдаёт Article.objects.all() целиком, работает ровно до тех пор, пока записей сотня. На десятках тысяч он кладёт и базу, и клиента: один запрос тянет мегабайты JSON. А открытый API без ограничения частоты — приглашение для скрапера и перебора паролей. Эти три механизма — не «украшения», а обязательная гигиена любого публичного списка.

Пагинация: три вида

Глобально пагинацию включают в настройках, задав класс и размер страницы:

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS":
        "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

DRF предлагает три стиля:

КлассПараметрыКогда
PageNumberPagination?page=3привычные страницы «1,2,3…»
LimitOffsetPagination?limit=20&offset=40гибкое «дай N, начиная с M»
CursorPagination?cursor=...большие/живые ленты, стабильность

Ответ с пагинацией оборачивает данные в конверт со счётчиком и ссылками:

{
  "count": 137,
  "next": "http://api/articles/?page=3",
  "previous": "http://api/articles/?page=1",
  "results": [ ... 20 объектов ... ]
}

PageNumberPagination прост, но на «живых» данных едет: если между запросами страниц вставили запись, объекты сдвигаются и одна и та же запись может попасть на две страницы или потеряться. CursorPagination лишён этого: он листает не по номеру, а по указателю на поле сортировки (обычно по времени), поэтому стабилен на постоянно растущих лентах, хотя и не даёт прыгнуть на произвольную страницу.

Фильтрация через django-filter

Чтобы клиент выбирал записи по полям (?author=7&status=published), подключают пакет django-filter:

REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
    ],
}

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filterset_fields = ["author", "status"]

Теперь работают точные фильтры по этим полям. Для более тонких правил (диапазоны, «содержит») описывают FilterSet:

import django_filters

class ArticleFilter(django_filters.FilterSet):
    min_views = django_filters.NumberFilter(
        field_name="views", lookup_expr="gte")
    title = django_filters.CharFilter(lookup_expr="icontains")

    class Meta:
        model = Article
        fields = ["author", "status"]
# теперь возможен запрос ?min_views=100&title=drf

Поиск и сортировка

Помимо точных фильтров DRF даёт два готовых backend-а. SearchFilter — полнотекстовый поиск по нескольким полям через один параметр ?search=. OrderingFilter — сортировка через ?ordering= (минус — по убыванию).

from rest_framework import filters

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ["title", "body"]
    ordering_fields = ["views", "created_at"]
# ?search=django           — ищет в title и body
# ?ordering=-views         — сначала самые просматриваемые

Эти три механизма свободно комбинируются: один запрос может одновременно фильтровать, искать и сортировать — каждый backend по очереди сужает queryset.

Throttling: ограничение частоты

Throttling задаёт потолок «сколько запросов за период». Глобально настраивают классы и нормы:

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "20/hour",
        "user": "1000/day",
    },
}

AnonRateThrottle ограничивает анонимов по IP, UserRateThrottle — залогиненных по их id. Когда лимит исчерпан, DRF отвечает 429 Too Many Requests и заголовком Retry-After. Для отдельных тяжёлых действий применяют ScopedRateThrottle — своя норма на «область»:

class PasswordResetView(APIView):
    throttle_scope = "password_reset"
    # в настройках: DEFAULT_THROTTLE_RATES = {"password_reset": "5/hour"}

Так дорогой и чувствительный эндпоинт (сброс пароля, отправка письма) получает жёсткий отдельный лимит, не затрагивая остальной API.

Как это работает под капотом

Все три механизма встроены в один и тот же конвейер вьюхи, но в разных точках. Фильтрация, поиск и сортировка применяются к queryset до выборки из БД: каждый backend из filter_backends получает текущий queryset и возвращает суженный, добавляя в SQL условия WHERE и ORDER BY. Затем пагинатор берёт уже отфильтрованный queryset и доклеивает LIMIT/OFFSET — поэтому из базы всегда тянется ровно одна страница, а не всё подряд. Throttling же срабатывает раньше всех, ещё в initial(), до обращения к данным: класс throttling по ключу (IP или id пользователя) хранит в кеше отметки времени недавних запросов и, если их больше нормы, прерывает обработку исключением Throttled с кодом 429. Из-за этого порядка throttling защищает в том числе и от запросов, которые иначе устроили бы тяжёлую выборку.

Частые ошибки

  • Отдавать список без пагинации. На больших таблицах один запрос грузит базу и забивает канал; страницы обязательны.
  • Брать PageNumberPagination для растущей ленты. Вставки сдвигают объекты между страницами; для лент берите CursorPagination.
  • Сортировать по любому полю по запросу клиента. Перечисляйте ordering_fields явно, иначе клиент отсортирует по неиндексированному полю и положит базу.
  • Забыть про throttling на публичных и логин-эндпоинтах. Без лимита открыты скрапинг и перебор; задайте хотя бы AnonRateThrottle.
  • Полагаться на throttling как на единственную защиту. Он сглаживает нагрузку и злоупотребления, но не заменяет аутентификацию и права.

Итоги

  • Пагинация режет список на страницы: PageNumberPagination, LimitOffsetPagination, CursorPagination (стабильна на живых лентах).
  • django-filter даёт фильтры по полям; тонкие правила (диапазоны, icontains) — через FilterSet.
  • SearchFilter (?search=) ищет по нескольким полям, OrderingFilter (?ordering=) сортирует; backend-ы комбинируются.
  • Throttling ограничивает частоту: AnonRateThrottle/UserRateThrottle/ScopedRateThrottle, ответ 429 при превышении.
  • Фильтры и пагинация сужают queryset до SQL-выборки; throttling срабатывает раньше всего и защищает от тяжёлых запросов.
Проверьте себя
1. Какой класс пагинации устойчив к вставкам в постоянно растущую ленту (объект не задвоится и не потеряется)?
APageNumberPagination
BLimitOffsetPagination
CCursorPagination
DЛюбой из них одинаково устойчив
2. Чем SearchFilter отличается от фильтрации через django-filter?
ASearchFilter делает полнотекстовый поиск по нескольким полям через один параметр ?search=, а django-filter даёт точные фильтры по конкретным полям
BЭто одно и то же
CSearchFilter сортирует, а не ищет
DSearchFilter работает только с числами
3. Что вернёт DRF, когда клиент превысил норму throttling?
A401 Unauthorized
B403 Forbidden
C429 Too Many Requests
D500 Internal Server Error