JavaRush /Курси /Модуль 3: Django /Використання DataLoader для оптимізації запитів

Використання DataLoader для оптимізації запитів

Модуль 3: Django
Рівень 25 , Лекція 3
Відкрита

Ми вже почали розбиратися в тому, як ефективно обробляти дані при роботі з GraphQL. Але чим більше даних, тим більше проблем, і від цього нікуди не дітися. Тож сьогодні ми поговоримо про нашу головну головну біль — проблему N+1 запитів — та метод її вирішення з використанням DataLoader.

Проблема N+1 запитів: чому це важливо?

Уявіть, що ви пишете запит, який має отримати список користувачів і їх коментарі у блозі. Наївне (і часто використовуване) рішення може виглядати так:

  1. Виконати один SQL-запит, щоб отримати всіх користувачів.
  2. Для кожного користувача виконати додатковий 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 зводиться до трьох основних концепцій:

  1. Batching (групування): замість виконання окремого запиту для кожного елемента, DataLoader групує запити та виконує їх за один раз.
  2. Caching (кешування): якщо одне й те саме значення запитується кілька разів, DataLoader повертає кешований результат.
  3. Lazy Execution (відкладене виконання): DataLoader збирає дані про запити доти, доки вони не будуть виконані, і виконує їх одночасно.

Встановлення DataLoader

Для початку потрібно встановити бібліотеку DataLoader:

pip install promise
pip install graphql-dataloader

Налаштування DataLoader у проєкті Django

  1. Імпортуємо DataLoader

У файл, де визначаються ваші GraphQL-резолвери, імпортуйте DataLoader:

from promise import Promise
from graphql_dataloader import DataLoader
  1. Створюємо 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 користувачів, виконує запит до бази даних, групує коментарі за користувачами та повертає дані.

  1. Використовуємо 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);

Додаткові оптимізації

  1. Кешування. DataLoader автоматично кешує результати протягом одного запиту. Це означає, що якщо два резолвери запитують однакові дані, другий запит візьме їх з кешу.

  2. 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.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ