Про пагінацію ви вже неодноразово чули протягом цього курсу, але все ж таки нагадаємо, що йдеться про механізм поділу великих даних на сторінки (або батчі), щоб клієнт отримував їх порціями, а не одним гігантським відповіддю.
Якщо ви коли-небудь використовували нескінченну стрічку в соцмережах або прокручували списки у великих інтернет-магазинах, то ви вже стикалися з пагінацією, навіть якщо не підозрювали про це.
Уявіть собі, що ваш сервер намагається відправити клієнту цілу базу даних за один раз. Неважливо, наскільки потужний ваш сервер — це не тільки з'їсть всю оперативну пам'ять, але й змусить користувача сумувати, дивлячись на білий екран завантаження. Ось тут і приходить на допомогу пагінація.
Існує кілька популярних стратегій пагінації в 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 з пагінацією. Ваші користувачі будуть задоволені, ваш сервер — стабільний, а ваш код — кристально ясний. 🎉
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ