Ми вже почали розбиратися в тому, як ефективно обробляти дані при роботі з GraphQL. Але чим більше даних, тим більше проблем, і від цього нікуди не дітися. Тож сьогодні ми поговоримо про нашу головну головну біль — проблему N+1 запитів — та метод її вирішення з використанням DataLoader.
Проблема N+1 запитів: чому це важливо?
Уявіть, що ви пишете запит, який має отримати список користувачів і їх коментарі у блозі. Наївне (і часто використовуване) рішення може виглядати так:
- Виконати один SQL-запит, щоб отримати всіх користувачів.
- Для кожного користувача виконати додатковий SQL-запит, щоб отримати його коментарі.
Якщо у нас 100 користувачів, це призведе до виконання 1 запиту для списку користувачів + 100 з апитів для отримання коментарів для кожного користувача. Раптово з одного красивого GraphQL-запиту у нас вийшло 101 SQL-запит. Це називається проблема N+1 запитів.
Приклад (без оптимізації)
query {
users {
id
username
comments {
id
content
}
}
}
У Django це може виглядати так:
def resolve_users(root, info):
return User.objects.all()
def resolve_comments(user, info):
return user.comments.all()
Для 100 користувачів це створить 101 SQL-запит: один запит для User.objects.all() і 100 запитів для отримання коментарів кожного користувача. Це не просто неефективно — це може вбити вашу базу даних.
DataLoader як рішення
DataLoader — це бібліотека, створена для вирішення проблеми N+1 запитів. Вона виконує батчинг (групування) запитів і кешування даних. З її допомогою ми можемо групувати запити до бази даних і виконувати їх за один раз.
Як працює DataLoader?
Суть DataLoader зводиться до трьох основних концепцій:
- Batching (групування): замість виконання окремого запиту для кожного елемента, DataLoader групує запити та виконує їх за один раз.
- Caching (кешування): якщо одне й те саме значення запитується кілька разів, DataLoader повертає кешований результат.
- Lazy Execution (відкладене виконання): DataLoader збирає дані про запити доти, доки вони не будуть виконані, і виконує їх одночасно.
Встановлення DataLoader
Для початку потрібно встановити бібліотеку DataLoader:
pip install promise
pip install graphql-dataloader
Налаштування DataLoader у проєкті Django
- Імпортуємо DataLoader
У файл, де визначаються ваші GraphQL-резолвери, імпортуйте DataLoader:
from promise import Promise
from graphql_dataloader import DataLoader
- Створюємо DataLoader для батч-запитів
Визначимо DataLoader, який буде отримувати коментарі для кількох користувачів за один запит:
class CommentLoader(DataLoader):
def batch_load_fn(self, user_ids):
# Отримуємо всі коментарі для списку user_ids за один запит
comments = Comment.objects.filter(user_id__in=user_ids)
# Групуємо коментарі за user_id
grouped_comments = {user_id: [] for user_id in user_ids}
for comment in comments:
grouped_comments[comment.user_id].append(comment)
# Повертаємо список коментарів для кожного user_id
return Promise.resolve([grouped_comments[user_id] for user_id in user_ids])
Тут batch_load_fn приймає список ID користувачів, виконує запит до бази даних, групує коментарі за користувачами та повертає дані.
- Використовуємо DataLoader у резолверах GraphQL
Тепер оновимо резолвер для коментарів, щоб використовувати DataLoader:
# Створюємо екземпляр CommentLoader
comment_loader = CommentLoader()
def resolve_users(root, info):
return User.objects.all()
def resolve_comments(user, info):
# Завантаження коментарів для вказаного користувача через DataLoader
return comment_loader.load(user.id)
Коли буде виконуватись запит GraphQL, DataLoader автоматично згрупує кілька звернень comment_loader.load(user.id) в один SQL-запит.
Перевірка роботи DataLoader
Запит GraphQL
query {
users {
id
username
comments {
id
content
}
}
}
SQL-запити до DataLoader:
SELECT * FROM users;
SELECT * FROM comments WHERE user_id=1;
SELECT * FROM comments WHERE user_id=2;
...
SELECT * FROM comments WHERE user_id=100;
SQL-запити після DataLoader:
SELECT * FROM users;
SELECT * FROM comments WHERE user_id IN (1, 2, 3, ..., 100);
Приклад використання DataLoader з вкладеними запитами
Припустимо, ми хочемо також отримати список лайків для коментарів. Це може швидко перерости в проблему N+1. Але з DataLoader ми можемо її вирішити.
Розширюємо DataLoader для лайків
class LikeLoader(DataLoader):
def batch_load_fn(self, comment_ids):
likes = Like.objects.filter(comment_id__in=comment_ids)
grouped_likes = {comment_id: [] for comment_id in comment_ids}
for like in likes:
grouped_likes[like.comment_id].append(like)
return Promise.resolve([grouped_likes[comment_id] for comment_id in comment_ids])
Оновлюємо резолвери
like_loader = LikeLoader()
def resolve_users(root, info):
return User.objects.all()
def resolve_comments(user, info):
return comment_loader.load(user.id)
def resolve_likes(comment, info):
return like_loader.load(comment.id)
Тепер наші SQL-запити виглядають так:
SELECT * FROM users;
SELECT * FROM comments WHERE user_id IN (1, 2, 3, ..., 100);
SELECT * FROM likes WHERE comment_id IN (1, 2, 3, ..., 500);
Додаткові оптимізації
Кешування. DataLoader автоматично кешує результати протягом одного запиту. Це означає, що якщо два резолвери запитують однакові дані, другий запит візьме їх з кешу.
Prefetch у Django ORM. Якщо ви знаєте, що дані знадобляться пізніше, використовуйте
select_relatedабоprefetch_relatedдля попереднього завантаження.
def resolve_users(root, info):
return User.objects.prefetch_related('comments')
Типові помилки та граблі
Помилка 1: забута ініціалізація DataLoader
Якщо ти створюєш DataLoader всередині резолвера, він втрачає свої переваги кешування та батчингу. Переконайся, що DataLoader створюється один раз для кожного запиту.
Помилка 2: перетворення DataLoader у складний шар логіки
DataLoader призначений тільки для групування запитів. Вся бізнес-логіка має знаходитися поза ним.
Практичне застосування
Використання DataLoader не просто робить ваш GraphQL API більш ефективним. Це також покращує масштабованість додатка. У реальних проєктах з тисячами користувачів і пов'язаних об'єктів DataLoader дозволяє уникнути перевантаження бази даних і збільшує швидкість відповіді сервера. Ці навички особливо цінуються на співбесідах на позиції backend-розробників, оскільки показують ваше вміння писати оптимізований код.
Для додаткового вивчення — ознайомтеся з офіційною документацією DataLoader.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ