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 з пагінацією. Ваші користувачі будуть задоволені, ваш сервер — стабільний, а ваш код — кристально ясний. 🎉

3
Опитування
Вступ до складних запитів у GraphQL, рівень 25, лекція 4
Недоступний
Вступ до складних запитів у GraphQL
Вступ до складних запитів у GraphQL
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ