JavaRush /Курси /Модуль 3: Django /Оптимізація GraphQL запитів

Оптимізація GraphQL запитів

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

Чесно кажучи, ніхто не хоче, щоб його API задихався під навантаженням або спалював серверні ресурси, як піч на дровах. Але навіть ідеально виглядаючі запити можуть виявитися справжніми пожирачами продуктивності. І ось тут починається магія оптимізації!

Сьогодні ми займемося хірургічною корекцією GraphQL-запитів (але без болю і анестезії — тільки корисні лайфхаки). Якщо ви хочете, щоб ваші запити літали, а сервер не плакав, вмощуйтеся зручніше!

Навіщо потрібна оптимізація?

GraphQL славиться своєю гнучкістю, дозволяючи клієнтам запитувати лише необхідні дані. Однак ця гнучкість може стати «сходами до пекла», якщо запити реалізовані неефективно. Наприклад:

  • Клієнти можуть запросити величезні вкладені дані, що призведе до безлічі SQL-запитів (відомий як проблема N+1).
  • Можна випадково створити складні запити, які гальмують всю базу даних.
  • Зайві дані передаються клієнту, збільшуючи затримки та обсяг мережевого трафіку.

Крім того, у реальних додатках, де дані надходять із кількох джерел (наприклад, SQL, Redis, зовнішні API), оптимізація має ключове значення для зниження часу виконання запитів.

Основні практики оптимізації запитів GraphQL

1. Мінімізація обсягу повернених даних

GraphQL дозволяє клієнту явно вказувати, які поля йому потрібні. Але це не привід розслаблятися на стороні сервера! Навіть якщо клієнт запитав тільки одне поле, сервер може побачити SQL-запит, що вибирає все підряд (ви зустрічали SELECT *?). Давайте розберемо приклад.

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = ("id", "username", "email")

class Query(graphene.ObjectType):
    all_users = graphene.List(UserType)

    def resolve_all_users(root, info):
        return User.objects.all()

Якщо клієнт запитає тільки username, сервер все одно завантажить id, username і email. Щоб це виправити, використовуйте only() і defer() в Django ORM:

def resolve_all_users(root, info):
    return User.objects.only("username")

Тепер дані будуть максимально легкими для завантаження.

2. Зменшення проблеми N+1 запитів

А тепер класика: уявіть, у вас є User, у якого багато Post. Ви хочете запитати список користувачів разом із їх постами:

query {
  allUsers {
    username
    posts {
      title
    }
  }
}

Якщо ви не застосуєте оптимізацію, GraphQL спочатку виконає запит для користувачів, а потім для кожного користувача окремий запит для його постів. Це називається проблема N+1, і вона уповільнює роботу застосунку.

Проста настройка select_related/prefetch_related вирішує цю проблему:

def resolve_all_users(root, info):
    return User.objects.prefetch_related("posts")

Тепер GraphQL виконає ОДИН запит для користувачів і ОДИН запит для всіх їх постів.

3. Використання полів, директив і фрагментів

Якщо у вашому GraphQL API є важкі обчислення чи дані, які найчастіше не потрібні, можна додати умовні поля. Наприклад, тільки якщо клієнт вказав директиву @include, поле буде повернено:

query {
  user(id: 1) {
    username
    email @include(if: true)
  }
}

Для цього в GraphQL-схемі створюється директива:

directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

У Django Graphene директиви вже підтримуються "з коробки", тож можна просто включити цю логіку у вашому резолвері.

4. Відкладене завантаження даних (Lazy Resolution)

Іноді важкі дані краще завантажувати вже після обробки основного запиту. Використовуйте утиліти, такі як Django DataLoader, щоб зібрати всі необхідні дані в один батч.

Приклад із використанням DataLoader (для завантаження постів усіх користувачів):

from promise import Promise
from promise.dataloader import DataLoader

class PostLoader(DataLoader):
    def batch_load_fn(self, keys):
        posts = Post.objects.filter(user_id__in=keys)
        post_map = {post.user_id: post for post in posts}
        return Promise.resolve([post_map.get(user_id) for user_id in keys])

# Використання DataLoader у запиті
def resolve_user_posts(user, info):
    return PostLoader().load(user.id)

Тепер всі пости для користувачів завантажуються одним запитом.

5. Використання фрагментів даних

Коли клієнти задають одні й ті ж запити в різних частинах застосунку, ви можете оптимізувати їх за допомогою фрагментів. Наприклад:

fragment UserFields on User {
  username
  email
}

query GetUsers {
  allUsers {
    ...UserFields
  }
}

query GetUser {
  user(id: 1) {
    ...UserFields
  }
}

На стороні сервера ви можете заздалегідь оптимізувати поля, вказані у фрагменті, щоб обробка була єдиною та ефективною.

Перевірка та профілювання запитів

Профілювання допомагає точно визначити, які частини вашого API гальмують. Використовуйте інструменти, такі як:

  1. django-silk – дозволяє відслідковувати SQL-запити та їх виконання.
  2. GraphQL Tracing – додає у відповідь інформацію про час виконання підзапитів.
  3. debug=True у Graphene – вмикає відладочну інформацію.

Приклад використання профілювання SQL-запитів:

from django.db import connection

def resolve_all_users(root, info):
    users = User.objects.prefetch_related("posts")
    query = str(connection.queries)
    print(query)  # Вивід усіх SQL-запитів
    return users

Це допоможе знайти проблемні області коду.

Приклади оптимізації: до і після

Початковий код (неоптимізований):

class Query(graphene.ObjectType):
    all_users = graphene.List(UserType)

    def resolve_all_users(root, info):
        return User.objects.all()

Запит:

query {
  allUsers {
    username
    posts {
      title
    }
  }
}

SQL-запити:

  1. SELECT * FROM user;
  2. SELECT * FROM posts WHERE user_id = 1;
  3. SELECT * FROM posts WHERE user_id = 2; ... і так далі для кожного користувача.

Оптимізований код:

def resolve_all_users(root, info):
    return User.objects.prefetch_related("posts").only("username")

SQL-запити:

  1. SELECT username FROM user;
  2. SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...);

Тепер ваші запити GraphQL готові до серйозних навантажень. Наступного разу ми поговоримо про DataLoader глибше, а також підключимо техніку для кешування запитів у разі частих звернень. А поки протестуйте свої знання на практиці: оптимізуйте ваш проект і поділіться результатами.

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