Предыдущая часть: “Почему нельзя разрешать поля GraphQL как конечные точки REST”
В предыдущей статье мы закончили на том, что при разрешении полей возникает проблема запроса N+1.
Теперь мы рассмотрим, как решить указанную проблему с помощью Dataloader.
Что такое Dataloader?
Dataloader — это библиотека, которая пакует последовательные запросы и “под капотом” составляет один запрос данных. Этот запрос может быть сделан к любому источнику данных, например к базе данных или веб-сервису.
Загрузчик данных принимает в качестве аргумента массив, обрабатывает данные с помощью этого аргумента и возвращает массив объектов.
Элемент с N-ым индексом возвращаемого массива будет рассматриваться DataLoader’ом как данные для N-го элемента во входном аргументе.
Давайте теперь реализуем postsLoader
.
const DataLoader = require('dataloader');
const postsLoader = new DataLoader( async (userIds) => {
// Assume, userIds = [ 1, 2 ]
let posts = await Post.findAll( { where: { userId: userIds } } );
// posts = [ {title: "A", userId: 1}, {title: "B", userId: 1}, {title: "C", userId: 2} ]
let postsGroupedByUser = userIds.map ( userId => {
return posts.filter( post => post.userId == userId );
});
// postsGroupedByUser = [
// [
// {title: "A", userId: 1},
// {title: "B", userId: 1}
// ],
// [
// {title: "C", userId: 2}
// ]
// ]
return postsGroupedByUser;
})
Теперь мы будем использовать этот postsLoader
для разрешения сообщений posts
. Обновленные resolvers
будут выглядеть следующим образом:
const resolvers = {
UserType: {
posts: (parent) => {
return postsLoader.load(parent.id);
}
},
Query: {
users: async () => {
try {
return await User.findAll()
} catch (e) {
console.log(e)
}
},
},
};
Когда клиент запрашивает пользователей вместе с полями сообщений, то для каждого пользователя, разрешенного в запросе “пользователи” (users
), этот распознаватель “сообщений” (posts
) будет вызван с родительским аргументом, равным объекту пользователя. Используя этого пользователя, мы можем найти сообщения.
Как видно из распознавателя поля posts
, мы все еще запрашиваем одно сообщение с помощью API load
загрузчика данных.
Несколько вызовов postsLoader.load()
будут упакованы в пакет, а затем только один раз вызовется postsLoader
.
Вот скриншот, показывающий запросы к базе данных, когда клиент запрашивает данные пользователей вместе с данными их сообщений.
Предостережение при реализации Dataloader
Dataloader не только пакует запросы, но и кэширует ответы. Он выполняет кэширование, чтобы гарантировать, что для извлечения тех же самых данных не будет выполнен другой запрос.
Проблема с этим кэшированием в том, что если распознаватель другого запроса попадает в этот загрузчик данных, когда обрабатывается первый запрос, то кэшированный результат вернется и второму запросу. Как понимаете, возврат кэшированных данных в совершенно другой запрос — это серьезная проблема.
Следовательно загрузчик данных всегда должен быть определен для каждого конкретного запроса.
Таким образом, разные экземпляры Dataloader будут использоваться разными запросами. Лучше всего определить их в контексте запроса GraphQL.
Новая реализация загрузчика данных будет выглядеть следующим образом:
const resolvers = {
UserType: {
posts: (parent, _, ctx) => {
return ctx.postsLoader.load(parent.id);
}
},
Query: {
users: async () => {
try {
return await User.findAll()
} catch (e) {
console.log(e)
}
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({req}) => {
return {
postsLoader: postsLoader
}
}
});
Надеюсь, статья была для вас полезной. В репозитории можно найти разобранную выше реализации решателя users
с помощью сервера Apollo GraphQL.
Читайте также:
- Добавление отношений в схему GraphQL
- Почему нельзя разрешать поля GraphQL как конечные точки REST
- Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL)
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи: Shriram Balakrishnan, “Solve N+1 query problem in GraphQL with Dataloader”