Предыдущая часть: “Почему нельзя разрешать поля 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 .

Вот скриншот, показывающий запросы к базе данных, когда клиент запрашивает данные пользователей вместе с данными их сообщений.

Так гарантируется, что мы только однократно попадем в базу данных, чтобы получить данные и, следовательно, проблема запроса N+1 будет решена.

Предостережение при реализации 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.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи: Shriram Balakrishnan, “Solve N+1 query problem in GraphQL with Dataloader”

Предыдущая статьяГенерируем образы Docker с помощью Spring Boot
Следующая статья7 трюков pandas для науки о данных