Про пагинацию вы уже неоднократно слышали в течение этого курса, но всё-таки напомним, что речь идёт о механизме разбиения больших данных на страницы (или батчи), чтобы клиент получал их порциями, а не одним гигантским ответом.
Если вы когда-либо использовали бесконечную ленту в соцсетях или прокручивали списки в крупных интернет-магазинах, то вы уже сталкивались с пагинацией, даже если не подозревали об этом.
Представьте себе, что ваш сервер пытается отправить клиенту целую базу данных на одном дыхании. Неважно, насколько мощный ваш сервер — это не только сожрёт всю оперативную память, но и заставит пользователя грустить, глядя на белый экран загрузки. Вот тут-то приходит на помощь пагинация.
Существует несколько популярных стратегий пагинации в GraphQL:
- Offset-based — наиболее простой вариант, где указываются
limit(количество элементов) иoffset(смещение). Хорошо подходит для небольших данных, но на больших объёмах может тормозить. - Cursor-based — более эффективный метод, использующий уникальный идентификатор (курсор) последнего полученного элемента. Позволяет избежать проблем с изменяющимися данными.
Давайте разберёмся, как это выглядит в коде и какие подходы лучше использовать в различных ситуациях!
Основные подходы к пагинации
В GraphQL есть несколько подходов к реализации пагинации. Мы рассмотрим два основных: offset/limit и cursor-based. Каждый из них имеет свои плюсы и минусы, и ваши предпочтения будут зависеть от требований приложения.
Подход offset/limit. Это самый простой и доступный способ пагинации. Здесь мы указываем:
- offset — откуда начинать выборку (например, пропустить первые 10 элементов).
- limit — количество элементов, которые нужно вернуть.
Подход cursor-based. Этот подход более сложный, но гораздо мощнее. Вместо того чтобы указывать сколько элементов пропустить, мы используем курсор — уникальный идентификатор, который указывает на конкретный элемент. Такой подход позволяет быть более точным в выборе данных и помогает избежать проблем при изменении содержимого базы.
Реализация пагинации: offset/limit
Давайте перейдем к практике. Представим, что у нас есть модель Post, где хранятся записи блога.
- В модели Django:
from django.db import models class Post(models.Model): title = models.CharField(max_length=255) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True)
- В схеме GraphQL создадим тип:
import graphene class PostType(graphene.ObjectType): id = graphene.ID() title = graphene.String() content = graphene.String() created_at = graphene.DateTime()
- Напишем запрос с пагинацией:
from graphene import ObjectType, List, Int from .models import Post class Query(ObjectType): posts = List(PostType, limit=Int(), offset=Int()) def resolve_posts(root, info, limit=None, offset=None): query = Post.objects.all() if offset is not None: query = query[offset:] if limit is not None: query = query[:limit] return query
Теперь вы можете запросить данные с помощью GraphQL-запроса:
query {
posts(limit: 5, offset: 10) {
id
title
createdAt
}
}
Этот запрос вернет 5 записей, начиная с 11-й (offset 10). Все просто и понятно.
Реализация пагинации: cursor-based
Теперь давайте перейдем к более профессиональному подходу. Для этого мы сделаем следующее:
- В качестве курсора будем использовать поле
created_at. - Реализуем логическую проверку: только записи, созданные после указанного времени, будут возвращены.
- Добавляем аргумент курсора:
from graphene import String class Query(ObjectType): posts = List(PostType, cursor=String(), limit=Int()) def resolve_posts(root, info, cursor=None, limit=None): query = Post.objects.order_by('created_at') # Обязательно сортируем! if cursor: query = query.filter(created_at__gt=cursor) # Только те, что "после" курсора if limit: query = query[:limit] return query
- Как это использовать?
Вот запрос:
query {
posts(cursor: "2023-01-01T12:00:00Z", limit: 5) {
id
title
createdAt
}
}
Этот запрос вернет первые 5 записей, созданных после 1 января 2023 года.
Вернем мета-данные о пагинации
Но погоди, есть нюанс! Пользователям, как правило, нужно знать, сколько всего элементов есть и есть ли следующая страница. Давайте добавим мета-данные.
- Расширяем наш запрос:
class PaginatedPosts(graphene.ObjectType): posts = List(PostType) has_next = graphene.Boolean() total_count = graphene.Int() class Query(ObjectType): paginated_posts = graphene.Field(PaginatedPosts, cursor=String(), limit=Int()) def resolve_paginated_posts(root, info, cursor=None, limit=None): query = Post.objects.order_by('created_at') total_count = query.count() if cursor: query = query.filter(created_at__gt=cursor) has_next = query.count() > limit if limit else False results = query[:limit] if limit else query return PaginatedPosts(posts=results, has_next=has_next, total_count=total_count)
- Теперь запрос будет выглядеть так:
query { paginatedPosts(cursor: "2023-01-01T12:00:00Z", limit: 5) { posts { id title createdAt } hasNext totalCount } }
А ответ:
{
"data": {
"paginatedPosts": {
"posts": [
{"id": "1", "title": "First Post", "createdAt": "2023-01-02T10:00:00Z"},
{"id": "2", "title": "Second Post", "createdAt": "2023-01-03T12:00:00Z"}
],
"hasNext": true,
"totalCount": 25
}
}
}
Особенности и ошибки
Иногда появляются проблемы:
- N+1 запросы. Если ваши
postsимеют связанные объекты (например, авторов), обязательно используйтеselect_related()илиprefetch_related()для оптимизации. - Потерянный курсор. Если данные в базе обновились, курсор может "потеряться". Это нормальная ситуация, которую нужно пояснить пользователю.
- Проверка лимитов. Не забывайте ограничивать
limit. Пользователь может запросить 10 миллиардов записей, а вы их случайно попытаетесь отправить.
Теперь вы готовы к написанию масштабируемых API с пагинацией. Ваши пользователи будут довольны, ваш сервер — стабилен, а ваш код — кристально ясен. 🎉
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ