GraphQL викликає резолвер для кожного поля кожного об'єкта. Запит
{
posts(first: 50) {
title
author { name }
}
}
виконає один запит за постами, а потім резолвер author - 50 разів, по одному на пост. Класичне N+1, тільки його не видно в коді: кожен резолвер окремо виглядає невинно.
DataLoader - патерн (і бібліотека з такою назвою від авторів GraphQL), що збирає звернення в пакет:
- резолвери не запитують базу одразу, а кажуть завантажувачу «мені потрібен автор 7», «мені потрібен автор 12»;
- завантажувач накопичує ключі протягом поточного «такту» виконання;
- потім робить один запит за всіма ключами (
WHERE id IN (7, 12, ...)) і роздає результати резолверам; - в межах запиту кешує вже завантажені значення - той самий автор не завантажиться двічі.
Головна вимога до функції пакетного завантаження: повернути результати в тому самому порядку, що й ключі, і з відповідною кількістю елементів (null для відсутніх).
У Laravel з Lighthouse це зроблено за вас для зв'язків:
type Post {
title: String!
author: User! @belongsTo
comments: [Comment!]! @hasMany
}
Директиви зв'язків Lighthouse пакетують запити до бази - зв'язок для 50 постів завантажиться одним запитом. Для даних, що не є зв'язками Eloquent (зовнішній сервіс, обчислення), Lighthouse дає змогу писати власні пакетні завантажувачі.
Типові пастки:
- власний резолвер з
->find()усередині поля обходить пакетування й повертає N+1; - вкладені списки з пагінацією в кожному елементі (
posts { comments(first: 5) }) складні для пакетування - «перші 5 коментарів кожного поста» не виражається простимIN, потрібні віконні функції чи окремі стратегії; - кеш завантажувача - на запит, не глобальний: інакше користувачі побачать чужі дані.
Як помітити: лічильник SQL-запитів на один GraphQL-запит (Telescope, Debugbar, Pulse) - тест, що перевіряє: кількість запитів не росте разом із кількістю елементів у списку.