Как мы решили самый большой недостаток GraphQL

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

Есть несколько рычагов, которые вы можете использовать, чтобы улучшить «ощущения» от вашего приложения, и вы можете разделить их на две категории:

  • Что вы можете сделать, чтобы ваше приложение работало быстрее: оптимистичные ответы, загрузчики и другие уловки UX, которые информируют или даже скрывают факт загрузки.
  • Что вы можете сделать, чтобы действительно ускорить ваше приложение: лучшее оборудование, кеш, оптимизированные алгоритмы и так далее.

В этой статье я собираюсь сосредоточиться на последнем и провести вас через путь устранения самого большого недостатка архитектур GraphQL.

Ваш сервер GraphQL не оптимизирован

Первое, что мы собираемся сделать, это немного покопаться в том, как GraphQL разрешает запросы и почему это ужасно неэффективно для большинства приложений. Начнем с простого запроса GraphQL:

… И самые простые решатели, о которых вы только можете подумать:

Обратите внимание, как я использую DataLoader в своем примере. Если вы не знакомы с тем, что такое DataLoader, подумайте о том, чтобы быстро прочитать эту статью.

Теперь, когда у нас есть тестовая установка, давайте посмотрим, как выглядит трассировка:

Проблема здесь довольно очевидна: 3 вызова базы данных выполняются последовательно, что делает запрос медленнее, чем он мог бы быть. Решателю Article.comments для разрешения нужен только идентификатор статьи, но мы ждем, пока сначала будет извлечена вся статья.

Далее мы увидим, как решить эту проблему распараллеливания, но сначала нам нужно решить вторую проблему, с которой мы сталкиваемся при таком наивном подходе: чрезмерная выборка. Чтобы проиллюстрировать, что это означает, давайте запросим только идентификаторы комментариев:

Теперь трассировка выглядит так:

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

Переосмысление решателей

Лучшим подходом является получение данных на уровне поля в ваших преобразователях:

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

Давайте еще раз запустим наш первый запрос и посмотрим на результаты трассировки:

Вы можете ясно видеть, что происходит некоторое распараллеливание и что мы значительно сократили время отклика.

С этим шаблоном вы также получаете бесплатную ленивую загрузку, что означает, что вы не делаете избыточную выборку, как в нашем предыдущем примере. Давайте еще раз запустим второй запрос, чтобы проиллюстрировать:

Как видите, мы выполнили только 1 вызов базы данных вместо 3 и значительно сократили общее время ответа.

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

Мы разработали GraphQL-Proxy с учетом простоты и эффективности (менее 200 LoC), наша работа была очень вдохновлена ​​DataLoader. Мы хотели, чтобы этот пакет не зависел от какой-либо библиотеки, имел четкие ошибки для разработчиков и имел 100% тестовое покрытие. Ваши отзывы всегда приветствуются!

Использование GraphQL-Proxy

Начните с установки GraphQL-Proxy с помощью npm:

npm i gql-proxy

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

Article.id теперь получает Article прокси и возвращает идентификатор этого прокси вместо того, чтобы просто возвращать полученный идентификатор. И поскольку это поведение является именно тем, что делает преобразователь по умолчанию в GraphQL, мы можем полностью опустить преобразователь идентификаторов:

Помните, что выполнение new Article(id) на самом деле ничего не делает, ни обращений к базе данных, ни проверок. Точно так же, как когда мы просто вернули идентификатор, но обернули его в класс.

Получение данных

Теперь мы собираемся реализовать преобразователи title и content. Нам нужно указать нашему прокси, как получать данные из нашей базы данных с помощью DataLoader. Первое, что вам нужно сделать, это добавить загрузчик в контекст GraphQL:

А при создании статьи просто перенаправьте загрузчики из контекста GraphQL в контекст прокси:

Вот и все, преобразователи title и content готовы к работе!

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

article.id просто возвращает значение, которое мы передали при создании экземпляра прокси без какого-либо вызова базы данных.

Мы ничего не указали для article.title и article.content, поэтому прокси-сервер возвращается к получению этих значений из базы данных с помощью объекта loaders, который мы передали в контекст.

Это работает, потому что мы следовали следующему соглашению:

  • Создавая экземпляр статьи, мы передали объект loaders в контекст.
  • Объект loaders имеет ключ, равный entityType из createProxy({ entityType }) с DataLoader в качестве значения.
  • Функция DataLoader .load(id) возвращает объект с ключами title и content.

Помните, что выборка данных происходит на уровне поля, это означает, что article.title и article.content являются обещаниями:

Добавление геттеров

В этом разделе мы реализуем преобразователь Article.comments. Точно так же, как article.title - это обещание, которое разрешается в заголовке статьи, мы ожидаем, что article.comments будет обещанием, которое, конечно же, преобразуется в массив ... Comment прокси!

Итак, первое, что нужно сделать, это создать прокси-класс Comment:

Теперь мы можем добавить загрузчики articleCommentIdsLoader и commentLoader в контекст GraphQL:

Помните, что когда мы создаем экземпляр статьи, мы передаем ей объект loaders. Таким образом, наш прокси-сервер статьи имеет доступ к articleCommentIdsLoader через свой контекст, нам просто нужно написать геттер для article.comments. Геттеры определяются при создании прокси-класса:

И все, теперь все конечные точки работают.

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

Мы надеемся, что эта статья поможет вам в ваших начинаниях, не стесняйтесь хлопать в ладоши, если вы нашли ее полезной, поделитесь ею с людьми, которых вы любите, и присылайте нам свои отзывы и комментарии ниже! ❤️