GraphQL - это современный язык запросов, который решает проблему избыточной выборки данных, связанных с REST API. По сравнению с REST, где конечная точка определяет тип и возвращаемые данные, GraphQL допускает определенные запросы, которые возвращают только запрошенные данные.

Однако проблема возникает с наивным вложенным запросом GraphQL, который выполняет несколько циклов обращения к базе данных при извлечении данных. В этой статье подробно рассматривается проблема N + 1 GraphQL и реализация DataLoader. Наконец, чтобы лучше понять, как работает DataLoader, изучите CachiQL, сверхлегкий пакет с нулевыми зависимостями.

Преобразователи GraphQL

О боже, есть системы типов, выполнение запросов, корневые поля и преобразователи! Что все это значит? Для начала сервер GraphQL принимает тип, который представляет точку (точки) входа в GraphQL API. Этот уникальный тип верхнего уровня называется корневым типом или типом запроса.

Сервер GraphQL выполняет каждый запрос GraphQL. Возвращаемые данные обычно возвращаются в форме, аналогичной запросу, обычно в формате JSON. Каждый запрос GraphQL должен иметь систему типов, как показано ниже.

{
  books{
    title
    Author{
      firstName
      lastName
    }
  }
}

Каждое поле в запросе GraphQL служит функцией предыдущего типа и возвращается, возвращая следующий тип. Функция называется преобразователем. После выполнения поля преобразователю присваивается имя для следующего значения.

Функции резольвера принимают четыре аргумента:

  • Obj: этот аргумент относится к предыдущему объекту, обычно не используется для корневого типа.
  • Аргументы: любые аргументы, предоставленные полю.
  • Контекст: значение, предоставляемое преобразователю с такой информацией, как доступ к базе данных.
  • Информация: сведения о конкретном поле или схеме.

Контекст и асинхронные преобразователи

Сосредоточившись на аргументе контекста функции преобразователя, помните, что доступ к базе данных требуется для загрузки любых запрошенных данных. Загрузка из базы данных асинхронная. Следовательно, возвращается обещание.

Как только обещание возвращается, можно вернуть новый объект или запрошенные данные. Таким образом, функция резолвера может принимать обещания, что важно отметить, потому что во время выполнения запроса GraphQL ожидает обещания, прежде чем продолжить.

Проблема GraphQL N + 1

В этом и заключается суть проблемы с GraphQL. Каждый преобразователь выполняется для каждого поля. Резольверы независимы друг от друга. Следовательно, наивный преобразователь не знает, были ли данные загружены ранее. Более того, наивный преобразователь не будет проверять дублирование данных после этого.

Обычно серверы разрешают запросы к каждому полю за раз. Таким образом, запрос загружает имя и фамилию автора в примере, а затем название каждой книги. В нашей небольшой базе данных шестнадцать книг, написанных тремя авторами.

В конечном итоге стандартное выполнение GraphQL - это поиск в глубину. Каждое дочернее поле разрешается перед полями верхнего уровня. Таким образом, именно поэтому наивное выполнение GraphQL привело бы к 16 запросам, полностью превышающим выборку необходимых данных.

DataLoader спешит на помощь

Распространенным решением проблемы GraphQL N + 1 с открытым исходным кодом является DataLoader. Ли Байрон из Facebook стремился написать DataLoader, чтобы решить эту общую проблему с GraphQL. Хотя изначально он был написан на Javascript с помощью Node.js, DataLoader теперь доступен в различных реализациях для разных языков.

По сути, DataLoader сильно зависит от отложенной загрузки данных. Помните, что в контекстном аргументе функции преобразователя вызовы и загрузка из базы данных являются асинхронными, возвращая обещания.

Пакетирование с обещаниями

Обещание - это обещание вернуть результат или, в данном случае, объект запрошенных данных. Однако с DataLoader, вместо того, чтобы сначала разрешать дочерние поля, преобразователь указывает с помощью Promise, что данные в конечном итоге будут возвращены, задерживая выборку в базу данных и переходя к следующему преобразователю на том же уровне.

Следовательно, загрузчики (отсюда и название DataLoader) собирают уникальные идентификаторы, необходимые для выборки данных из преобразователей. Затем DataLoader группирует эти ключи, используя очередь в Node.js (process.nextTick) для ожидания всех поставленных в очередь обещаний.

Наивный загрузчик данных: CachiQL

Вдохновением для создания CachiQL, сверхлегкого пакета NPM, является глубокое понимание реализации DataLoader. В целом цель состоит не в том, чтобы заменить DataLoader, а в том, чтобы помочь другим понять смысл DataLoader.

Установите пакет CachiQL с помощью npm.

npm install --save cachiql

Как использовать CachiQL

Помните, что пакетная обработка - это основная функция пакета CachiQL. Возвращаясь к тому, как GraphQL стандартизирует преобразователи, мы знаем, что возникает проблема N + 1. Решатели выполняют запрос поиска в глубину, выполняя несколько обращений к базе данных.

Ниже приведен пример использования пакета CachiQL в GraphQL.

const cachiql = require(‘cachiql’);

const schema = new GraphQLSchema({
  query: RootQueryType
});

app.use(
  '/graphql',
  graphqlHTTP({
    schema: schema,
    graphiql: true,
    context: {
      authorLoader: new Cachiql(AuthorLoader),
      bookLoader: new Cachiql(BookLoader),
      cachedData: []
    }
  })
);

Пакетирование в CachiQL

В CachiQL конструктор класса включает пакетный загрузчик, который принимает массив ключей, возвращает обещание для каждого ключа и, наконец, разрешает обещания либо вернуть массив значений, либо отклоненные обещания. Загрузчик позволяет загружать отдельные значения перед выполнением пакета.

const batchAuthors = async (ids) => {
  try {
    const authors = await Author.find({ _id: { $in: ids } });
    return authors;
  } catch (err) {
    throw new Error('There was an error getting the Authors');
  }
};

module.exports = batchAuthors;

const batchBooks = async (ids) => {
  try {
    const books = await Book.find({ _id: { $in: ids } });
    return books;
  } catch (err) {
    throw new Error('There was an error getting the Books');
  }
};

module.exports = batchBooks;

Делать вклад

Наша команда CachiQL всегда доступна для обратной связи и открыта для увлеченных разработчиков, которые хотят внести свой вклад в наш продукт с открытым исходным кодом. Если вы хотите внести свой вклад или сотрудничать, посетите наш GitHub и свяжитесь с нашей командой.

Ванесса Лутц - GitHub / LinkedIn

Фахад Шейх - GitHub / LinkedIn

Каден Джонсон - GitHub / LinkedIn

Эдди Сапата - GitHub / LinkedIn