Если вы использовали GraphQL, вы, вероятно, сталкивались или слышали проблему N + 1.
Что это?
Когда мы выполняем следующий запрос, чтобы получить 100 лучших обзоров и соответствующие имена авторов, мы сначала делаем один вызов, чтобы получить 100 записей обзора из базы данных, а затем для каждого обзора мы делаем еще один вызов базы данных для получения сведений о пользователе. учитывая ID автора.
Запрос
{ top100Reviews { body author { name } } }
Схема
const typeDefs = gql` type User { id: ID! name: String } type Review { id: ID! body: String author: User product: Product } type Query { top100Reviews: [Review] } `;
Резольверы
const resolver = { Query: { top100Reviews: () => get100Reviews(), }, Review: { author: (review) => getUser(review.authorId), }, };
Мне нужно сделать много звонков. Кроме того, это большие накладные расходы на сеть.
DataLoader может помочь вам сократить накладные расходы, объединяя каждую отдельную загрузку сведений о пользователях в один вызов. Нам нужно просто предоставить реализацию пакетной функции.
const DataLoader = require('dataloader'); const userLoader = new DataLoader(ids => batchGetUsers(ids)); const resolvers = { Query: { top100Reviews: () => get100Reviews(), }, Review: { author: (review) => userLoader.load(review.authorId), }, };
Когда вызывается каждый userLoader.load(review.authorId)
, он группируется и в конечном итоге вызывает batchGetUsers()
. Таким образом, вместо 100 отдельных обращений к базе данных для получения сведений о пользователе то же самое можно сделать одним вызовом.
Какое отношение имеет N + 1 к Федерации Аполлонов?
Планировщик запросов в Apollo частично, но не полностью обрабатывает N + 1 за вас. Используя демонстрацию федерации в качестве примера, у нас есть служба для пользователей, которая имеет дело только с пользователями.
// User service const typeDefs = gql` type User @key(fields: "id") { id: ID! name: String username: String } `; const resolvers = { User: { __resolveReference(object) { return getUser(object.id); } } };
Другая служба проверки занимается только отзывами, хотя внутри Review
у нас есть ссылка на User
тип, который, как мы знаем, будет обрабатываться пользовательской службой.
// Review service const typeDefs = gql` type Review @key(fields: "id") { id: ID! body: String author: User product: Product } extend type User @key(fields: "id") { id: ID! @external } extend type Product @key(fields: "upc") { upc: String! @external } extend type Query { top100Reviews: [Review] } `; const resolvers = { Review: { author(review) { return { __typename: "User", id: review.authorId }; } }, // ... };
На шлюзе эти отдельные схемы объединяются в единую схему, и если мы выполняем тот же запрос, что и раньше, top100Reviews
запрос делегируется и разрешается службой проверки, а разрешение author
делегируется пользовательский сервис.
{ top100Reviews { body author { name } } }
Сначала мы могли бы подозревать, что при разрешении author
будет выполнено несколько обращений к пользовательской службе, но это не так. Мы можем проверить утверждение, зарегистрировав тело входящего запроса в параметре context.
// User service const server = new ApolloServer({ schema: buildFederatedSchema([{ typeDefs, resolvers}]), context: ({ req }) => { console.log(JSON.stringify(req.body, null, 2)); }, });
Мы видим, что планировщик запросов объединяет все идентификаторы пользователей в единый массив и отправляет массив представлений в службу пользователя, которая отвечает за разрешение ссылки.
{ "query": "query ($representations: [_Any!]!) {\n _entities(representations: $representations) {\n ... on User {\n name\n }\n }\n}", "variables": { "representations": [ { "__typename": "User", "id": "1" }, { "__typename": "User", "id": "2" }, { "__typename": "User", "id": "1" }, { "__typename": "User", "id": "2" }, // ... too long to list them all ] } }
Затем служба пользователей сопоставляет каждый отдельный идентификатор и вызывает __resolveReference()
для разрешения каждого идентификатора пользователя. Хотя шлюз не выполняет несколько вызовов пользовательской службы, для разрешения ссылок по-прежнему требуется несколько вызовов базы данных из-за getUser()
.
// User service const resolvers = { User: { __resolveReference(object) { return getUser(object.id); // Bad } } };
Решение
Мы можем исправить это, используя тот же userLoader.load()
вместо getUser()
. Загрузчик данных группирует все идентификаторы пользователей и эффективно выполняет один вызов базы данных.
// User service const resolvers = { User: { __resolveReference(object) { return userLoader.load(object.id); // Good } } };
Резюме
Мы объясняем ловушку запроса N + 1 и способы решения проблемы с помощью загрузчика данных. Мы также обнаруживаем, что Apollo Gateway эффективно обрабатывает делегирование службам путем пакетной обработки ссылок, но наш преобразователь по-прежнему должен явно использовать загрузчик данных, чтобы избежать множественных обращений к базе данных.