JavaRush /Курсы /Модуль 3: Django /Пагинация в GraphQL

Пагинация в GraphQL

Модуль 3: Django
25 уровень , 4 лекция
Открыта

Про пагинацию вы уже неоднократно слышали в течение этого курса, но всё-таки напомним, что речь идёт о механизме разбиения больших данных на страницы (или батчи), чтобы клиент получал их порциями, а не одним гигантским ответом.

Если вы когда-либо использовали бесконечную ленту в соцсетях или прокручивали списки в крупных интернет-магазинах, то вы уже сталкивались с пагинацией, даже если не подозревали об этом.

Представьте себе, что ваш сервер пытается отправить клиенту целую базу данных на одном дыхании. Неважно, насколько мощный ваш сервер — это не только сожрёт всю оперативную память, но и заставит пользователя грустить, глядя на белый экран загрузки. Вот тут-то приходит на помощь пагинация.

Существует несколько популярных стратегий пагинации в GraphQL:

  • Offset-based — наиболее простой вариант, где указываются limit (количество элементов) и offset (смещение). Хорошо подходит для небольших данных, но на больших объёмах может тормозить.
  • Cursor-based — более эффективный метод, использующий уникальный идентификатор (курсор) последнего полученного элемента. Позволяет избежать проблем с изменяющимися данными.

Давайте разберёмся, как это выглядит в коде и какие подходы лучше использовать в различных ситуациях!

Основные подходы к пагинации

В GraphQL есть несколько подходов к реализации пагинации. Мы рассмотрим два основных: offset/limit и cursor-based. Каждый из них имеет свои плюсы и минусы, и ваши предпочтения будут зависеть от требований приложения.

Подход offset/limit. Это самый простой и доступный способ пагинации. Здесь мы указываем:

  • offset — откуда начинать выборку (например, пропустить первые 10 элементов).
  • limit — количество элементов, которые нужно вернуть.

Подход cursor-based. Этот подход более сложный, но гораздо мощнее. Вместо того чтобы указывать сколько элементов пропустить, мы используем курсор — уникальный идентификатор, который указывает на конкретный элемент. Такой подход позволяет быть более точным в выборе данных и помогает избежать проблем при изменении содержимого базы.

Реализация пагинации: offset/limit

Давайте перейдем к практике. Представим, что у нас есть модель Post, где хранятся записи блога.

  1. В модели 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)
  1. В схеме GraphQL создадим тип:
    import graphene
    
    class PostType(graphene.ObjectType):
        id = graphene.ID()
        title = graphene.String()
        content = graphene.String()
        created_at = graphene.DateTime()
  1. Напишем запрос с пагинацией:
    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

Теперь давайте перейдем к более профессиональному подходу. Для этого мы сделаем следующее:

  1. В качестве курсора будем использовать поле created_at.
  2. Реализуем логическую проверку: только записи, созданные после указанного времени, будут возвращены.
  1. Добавляем аргумент курсора:
    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
  1. Как это использовать?

Вот запрос:

query {
  posts(cursor: "2023-01-01T12:00:00Z", limit: 5) {
    id
    title
    createdAt
  }
}

Этот запрос вернет первые 5 записей, созданных после 1 января 2023 года.

Вернем мета-данные о пагинации

Но погоди, есть нюанс! Пользователям, как правило, нужно знать, сколько всего элементов есть и есть ли следующая страница. Давайте добавим мета-данные.

  1. Расширяем наш запрос:
    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)
  1. Теперь запрос будет выглядеть так:
    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
    }
  }
}

Особенности и ошибки

Иногда появляются проблемы:

  1. N+1 запросы. Если ваши posts имеют связанные объекты (например, авторов), обязательно используйте select_related() или prefetch_related() для оптимизации.
  2. Потерянный курсор. Если данные в базе обновились, курсор может "потеряться". Это нормальная ситуация, которую нужно пояснить пользователю.
  3. Проверка лимитов. Не забывайте ограничивать limit. Пользователь может запросить 10 миллиардов записей, а вы их случайно попытаетесь отправить.

Теперь вы готовы к написанию масштабируемых API с пагинацией. Ваши пользователи будут довольны, ваш сервер — стабилен, а ваш код — кристально ясен. 🎉

1
Задача
Модуль 3: Django, 25 уровень, 4 лекция
Недоступна
Реализация offset/limit пагинации
Реализация offset/limit пагинации
1
Задача
Модуль 3: Django, 25 уровень, 4 лекция
Недоступна
Реализация cursor-based пагинации
Реализация cursor-based пагинации
3
Опрос
Введение в сложные запросы в GraphQL, 25 уровень, 4 лекция
Недоступен
Введение в сложные запросы в GraphQL
Введение в сложные запросы в GraphQL
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Олег Е Уровень 90
5 октября 2025
👍