Стоит ли рассматривать GraphQL?

Вы задаетесь вопросом, стоит ли вам использовать GraphQL в вашем проекте? Ваши разработчики спорят из-за таких аргументов, как «GraphQL — это будущее» и «REST просто проще»? У меня были бесконечные дискуссии с моей командой, которые я подытожу здесь.

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

1. Начните с «Почему?»

Первый вопрос, который я задаю себе, прежде чем рассматривать новую технологию: «Зачем мне ее использовать?»

Для GraphQL лучший способ ответить — вернуться к исходной проблеме, с которой столкнулся Facebook. Исходный пост в сентябре 2015 года прекрасно объясняет проблему.

«Еще в 2012 году мы начали переделывать нативные мобильные приложения Facebook. В то время наши приложения для iOS и Android представляли собой тонкую оболочку для просмотров нашего мобильного веб-сайта. Хотя это приблизило нас к платоническому идеалу мобильного приложения «напиши один раз, запускай где угодно», на практике оно вывело наши мобильные веб-приложения за пределы их возможностей. По мере того как мобильные приложения Facebook становились все более сложными, они страдали от низкой производительности и часто зависали. Когда мы перешли на собственные модели и представления, нам впервые понадобилась версия новостной ленты с данными API, которая до этого момента поставлялась только в формате HTML.

Мы оценили варианты доставки данных Ленты новостей в наши мобильные приложения, включая ресурсы сервера RESTful и таблицы FQL (API Facebook, похожий на SQL). Мы были разочарованы различиями между данными, которые мы хотели использовать в наших приложениях, и запросами к серверу, которые им требовались. Мы не думаем о данных с точки зрения URL-адресов ресурсов, вторичных ключей или таблиц соединений; мы думаем об этом с точки зрения графа объектов.

Facebook столкнулся с конкретной проблемой и создал собственное решение: GraphQL. Чтобы представить данные в виде графика, они разработали иерархический язык запросов. Другими словами, GraphQL естественным образом следует отношениям между объектами. Вы можете иметь вложенные объекты и возвращать их все одним сетевым вызовом.

Они построили протокол, а не базу данных. У Facebook уже было хранилище. Каждое поле GraphQL на сервере поддерживается произвольной функцией, которая изолирует бизнес-логику от хранилища.

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

Очень легко понять, как GraphQL решает проблемы Facebook. Остается вопрос: «Решает ли это вашу проблему?»

2. Преимущества GraphQL

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

  • Один запрос, много ресурсов: по сравнению с REST, где вам нужно сделать несколько сетевых вызовов для каждой конечной точки, с GraphQL вы можете запрашивать все свои ресурсы одним вызовом.
  • Точная выборка данных: GraphQL сводит к минимуму объем данных, передаваемых по сети, путем избирательного выбора данных в зависимости от потребностей клиентского приложения. Таким образом, мобильный клиент с маленьким экраном может получить меньше информации.
  • Строгая типизация. Каждый объект запроса, ввода и ответа имеет тип. В веб-браузерах отсутствие шрифта в JavaScript стало слабостью, которую пытались компенсировать многие инструменты (Dart от Google, TypeScript от Microsoft). GraphQL позволяет вам обмениваться типами между бэкэндом и интерфейсом.
  • Улучшенный инструментарий и удобство для разработчиков.К серверу Introspective можно запрашивать типы, которые он поддерживает, что позволяет использовать обозреватель API, автозаполнение и предупреждения редактора. Вам больше не нужно полагаться на разработчиков серверной части для документирования их API. Вы можете просто изучить конечные точки и получить нужные данные.
  • Бесплатная версия: форма возвращаемых данных полностью определяется запросом клиента, поэтому серверы стали проще. Когда вы добавляете новые функции продукта, на стороне сервера можно добавить дополнительные поля, не затрагивая существующие клиенты.

Я прекрасно понимал эти преимущества и сам был в восторге от этой технологии.

3. Медовый месяц

Наша мобильная команда активно поддерживала GraphQL внутри компании. Нашей команде настольного интерфейса также понравилась идея типов. У нас уже был REST API, но мы внедрили эту технологию в 2019 году. Команда потратила время на создание новых конечных точек для GraphQL. Мы выбрали библиотеку Apollo, которая предлагает клиентам React.js, Kotlin и Swift.

Первую конечную точку было действительно легко настроить. Сервер Apollo хорошо работает с express.js на бэкэнде, а две конечные точки API Rest и GraphQL могут работать вместе в одном приложении.

Интерфейсный код стал намного проще с GraphQL благодаря «одному запросу много ресурсов». Рассмотрим ситуацию, когда пользователь хочет получить сведения о конкретном исполнителе, например (имя, идентификатор, треки и т. д.). При использовании традиционного интуитивно понятного шаблона REST потребуются взаимные запросы между двумя конечными точками /artists и /tracks, которые затем передняя часть должна будет объединить. Однако с помощью GraphQL мы можем определить все данные, которые нам нужны, в запросе, как показано ниже:

artists(id: "1") {
  id
  name
  avatarUrl
  tracks(limit: 2) {
    name
    urlSlug
  }
}

Поскольку первые конечные точки шли хорошо, мы начали добавлять гораздо больше, в 2020 году у нас было более 50.

4. Соблазн микрооптимизации

Через два года я понял, что некоторые преимущества GraphQL не приносят дополнительной ценности нашему проекту.

4.1. Один запрос, много ресурсов

Как упоминалось ранее, основным преимуществом этого шаблона является упрощение клиентского кода. Но некоторые инженеры хотели использовать его для оптимизации сетевых вызовов и ускорения загрузки приложения.

Я не думаю, что это действительная оптимизация:

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

4.2. Точная выборка данных

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

Не думаю, что этот аргумент уместен.

  • Не у всех есть мобильное и десктопное приложение. Это преимущество может даже не относиться к вашему проекту. Я проигнорирую аргумент «А как насчет умных часов?»
  • Теоретически это интересно, но на практике десктопное и мобильное приложения похожи, да и данных много не экономит. Давайте посмотрим на эти два примера: Spotify и Amazon.

Для экрана альбома Spotify на рабочем столе есть только три дополнительных поля (количество воспроизведений, продолжительность трека и продолжительность альбома, поэтому на один JSON сохраняется около 30 байтов). Для Amazon на экране рабочего стола есть только два дополнительных поля данных. Приложение Spendesk такое же, и на самом деле оно не использует эту функцию для оптимизации размера полезной нагрузки.

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

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

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

5. Простые неудобства

5.1. Загадка со строгой типизацией

Как и WSDL несколько лет назад, GraphQL определяет все типы, команды и запросы API в файле graphql.schema. Я большой любитель печатать. Однако я обнаружил, что набор текста с помощью GraphQL может сбивать с толку.

Во-первых, много дублирования. GraphQL определяет тип в схеме, но нам пришлось переопределить аналогичные типы для нашего бэкенда (TypeScript с node.js) и наших мобильных приложений (в Swift и Kotlin).

Для решения этой проблемы появились следующие два решения:

а. Ввод кода

Первое решение (например, nexus, typegraphql) позволяет вам объявлять свои типы в TypeScript, а затем генерировать на их основе схему GraphQL.

Наша команда попробовала нексус в 2020 году и бросила его через месяц. Код был запутанным и плохо работал с zod.js, который мы также использовали для набора текста. Он не поддерживал такие функции, как федерация, и нулевые значения работали некорректно. Попытка отладить сгенерированную схему GraphQL оказалась непростой. Это был ужасный опыт, и я бы не рекомендовал его.

б. Первый ввод схемы

Другое решение противоположно; его использует Apollo (JS), или Ариадна (Python), или CodeGen. Сначала вы строите свою схему, а скрипт генерирует типы из заданного schema.graphql файла.

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

По-прежнему существуют некоторые проблемы с «необнуляемыми» типами, которые TypeScript не понимает как необязательные, enum в GraphQL, которых нет в TypeScript, и преобразователи возвращают запутанные типы.

Enum в GraphQL оказывается двумя типами TypeScript, которые выглядят следующим образом:

enum FourEyesProcedureStatus {
  VALIDATION_PENDING
  CANCELLED
  VALIDATED
  REJECTED
export type FourEyesProcedureStatus =
  | 'CANCELLED'
  | 'REJECTED'
  | 'VALIDATED'
  | 'VALIDATION_PENDING';

export type FourEyesStatusEnum =
  | 'CANCELLED'
  | 'REJECTED'
  | 'VALIDATED'
  | 'VALIDATION_PENDING';

Типы, сгенерированные для распознавателей, запутаны. Проблема заключается в вложенных объектах, где распознаватель не возвращает полный объект. Вместо этого он вернет частичный объект и позволит другому resolver заполнить другие поля.

Типизированные языки не поддерживают этот шаблон из коробки. В TypeScript Codegen в конечном итоге добавляет в ваш код немного any и создает действительно сложные типы.

blockAccount: async (_, args, context): Promise<any> => {}

export type BlockAccountResultResolvers<
  ContextType = any,
  ParentType extends ResolversParentTypes['BlockAccountResult'] = ResolversParentTypes['BlockAccountResult'],
> = ResolversObject<{
  account?: Resolver<ResolversTypes['AccountDetails'], ParentType, ContextType>;
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

Для нашего REST API мы просто определяем внутренние типы и некоторые внешние DTO. Он все еще имеет некоторое дублирование, но остается намного проще, чем GraphQL.

5.2. Улучшенные инструменты, кроме Google Chrome

Я скептически отнесся к инструментам, потому что, когда мы начали использовать GraphQL, такие продукты, как insomnia или Postman, не поддерживали GraphQL. Сейчас стало лучше, и все они делают это по умолчанию.

Есть одна вещь, которая меня до сих пор беспокоит. Отладка сложнее. Просто взгляните на эти два веб-сайта: на одном везде используется GraphQL, а на другом есть конечные точки REST. В инспекторе Chrome сложно найти то, что вы ищете, так как все конечные точки выглядят одинаково. В REST вы знаете, какие данные вы извлекаете, просматривая URL-адрес.

5.3. Нет поддержки кодов ошибок

REST позволяет вам использовать коды ошибок HTTP, такие как 404 not found, 400 неверных запросов и т. д., но GraphQL этого не делает. GraphQL заставляет вас возвращать код 200 с сообщением об ошибке в полезной нагрузке ответа. Чтобы определить, какая конечная точка вышла из строя, вы должны проверить каждую полезную нагрузку.

Кроме того, некоторые объекты могут быть пустыми, потому что их не удалось найти или произошла ошибка. И трудно понять разницу.

5.4. Мониторинг

Что касается предыдущей темы, отслеживать ошибки HTTP действительно легко по сравнению с GraphQL, потому что все они имеют свой собственный код ошибки. Для GraphQL нет простого решения. Apollo работает над этим, и я уверен, что скоро найдется решение.

6. Большая жирная ложь

Предыдущие проблемы - неудобства. Тем не менее, после трех лет использования GraphQL я обнаружил и более серьезные проблемы.

6.1. Без версии

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

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

6.2. Пагинация

На страницах GraphQL Best Practices мы можем прочитать следующее:

Спецификация GraphQL намеренно умалчивает о нескольких важных проблемах, с которыми сталкиваются API, таких как работа с сетью, авторизация и разбиение на страницы.

Это удобно 😅. В целом, вы обнаружите, что разбиение на страницы в GraphQL болезненно.

6.3. Кэширование

Кэширование направлено на более быстрое получение ответа сервера за счет сохранения результатов предыдущих вычислений. В REST URL-адреса — это уникальные идентификаторы ресурсов, к которым вы пытаетесь получить доступ. Таким образом, вы можете кэшировать на уровне ресурсов. Кэширование встроено в спецификацию HTTP. Браузер и мобильные устройства также могут использовать этот идентификатор URL и кэшировать ресурсы локально (как они делают для изображений или CSS).

В GraphQL это усложняется, поскольку каждый запрос может быть разным, даже если он работает с одним и тем же объектом. Для этого потребуется кэширование на уровне поля, чего нелегко достичь с помощью GraphQL, поскольку он использует одну конечную точку. Такие библиотеки, как Prisma и Dataloader, были разработаны, чтобы помочь в подобных сценариях, но они все еще далеки от возможностей REST.

6.4. N+1 проблем

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

query {
  authors {
    name
    books {
      title
    }
  }
}

resolver будет выглядеть так:

resolvers = {
  Query: {
    authors: async () => {
      return ORM.getAllAuthors()
    }
  }
  Author: {
    books:  async (authorObj, args) => {
      return ORM.getBooksBy(authorObj.id)
    }
  },
}

Поскольку распознаватель книг вызывается на id, запрос станет

SELECT * FROM authors; 

SELECT * FROM books WHERE author_id == 1;
SELECT * FROM books WHERE author_id == 2; 
SELECT * FROM books WHERE author_id == 3; 

Нам нужно было бы сделать вызов БД для каждого автора, потеряв возможность использовать функции SQL, такие как SELECT * FROM books WHERE author_id in (1,2,3), где вы будете делать N+1 запросов к БД вместо 2. Одним из решений является загрузчик данных, но он заставляет вас чтобы добавить еще один уровень сложности в ваш код, усложнив отладку проблем с производительностью.

6.5. Типы носителей

У нас есть API для загрузки документов и их отображения. GraphQL по умолчанию не поддерживает загрузку документов с multipart-form-data. Apollo работал над решением под названием загрузка файлов, но его сложно настроить. Кроме того, GraphQL не поддерживает заголовок типов мультимедиа, когда вы хотите получить документ, что позволяет вашему браузеру правильно отображать файл.

6.6. Безопасность

С GraphQL вы можете запрашивать именно то, что хотите, но вы должны знать, что это приводит к сложным последствиям для безопасности. Если злоумышленник попытается отправить дорогостоящий вложенный запрос, чтобы перегрузить ваш сервер, вы будете уязвимы для атак типа DDoS (атака на отказ в обслуживании).

Они также могут получить доступ к полям, которые не должны быть раскрыты. С REST вы можете контролировать разрешения на уровне URL. Для GraphQL это должно быть на уровне поля.

user {
  username <-- can be seen by everyone
  email <-- private
  post {
    title <-- some posts are private
  }
}

7. Кошмар растущей схемы

По мере роста графа данных вы можете разделить данные между несколькими службами. Stitching и Federation — это решения, позволяющие разделить схему GraphQL на более мелкие. Но есть главное отличие.

7.1. Федерация

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

Благодаря федерации (поддерживаемой Apollo) каждый сервис отвечает за поддержку своей части схемы GraphQL. Затем сервер федерации извлекает каждую схему и объединяет их в одну.

Федерация помогает организациям с несколькими командами GraphQL объединять глобальную схему GraphQL.

Федерация работает хорошо, если вы остаетесь в границах GraphQL. Все напечатано, так что он может легко обнаружить ошибки. Вы можете определить @key для объединения объектов в нескольких схемах. Но по умолчанию он не поддерживает горячую перезагрузку. Это означает, что вам либо нужно перезапускать шлюз каждый раз при изменении подграфа, либо вам нужно использовать Управляемую федерацию, проприетарное решение Apollo.

7.2 Сшивание

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

Сшивание (при поддержке mesh, hasura или stepzen) помогает объединить несколько источников данных, поддерживаемых центральной командой API данных. Вы определяете одну основную схему GraphQL на шлюзе и используете преобразователь для автоматического извлечения этих данных из каждой службы, предоставляющей данные (gRPC, REST, SQL).

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

  • Прямая демонстрация схемы внутренней базы данных напрямую через ваш API немного опасна, поскольку создает сильную связь с хранилищем. ORM, как и Prisma или Graphile, имеет встроенную интеграцию с GraphQL, продвигая эту идею дальше.
  • Другие источники данных не типизированы, в отличие от GraphQL. Таким образом, вам необходимо учитывать дополнительную сложность, когда некоторые объекты имеют одно и то же имя или когда вы хотите объединить данные по заданному ключу.
  • Вы можете смешивать сшивание и федерацию, делая это еще более странным.

Честно говоря, оба они довольно сложны. Этот выбор не только технический; он связан со структурой вашей организации. Если команды независимы и вы не хотите применять GraphQL везде, сшивка — лучшее решение.

8. Экосистема

По моему мнению, экосистема недостаточно зрелая, и это последняя проблема.

GraphQL был создан внутри Facebook в 2012 году, а исходный код был открыт в 2015 году. В 2019 году Facebook создал GraphQL Foundation, нейтральную некоммерческую организацию, которая поддерживает спецификации и базовую реализацию для Node.js (graphql.js ).

С тех пор появилось много действующих лиц, и экосистема стала более сложной. По состоянию на май 2021 года существует четыре другие реализации только для Node.js (Apollo, Express, Yoga, Helix), шесть для Go и четыре для Python.

Facebook по-прежнему активно работает с такими инструментами, как ретрансляция, но они не принимают окончательное решение. Экосистема теперь насчитывает двух других основных игроков:

  • Гильдия — это группа разработчиков с открытым исходным кодом, работающих над yoda server, mesh или codegen.
  • Apollo — это частная компания, которая открывает исходный код своего сервера и клиентов (Kotlin, iOS и React), но продает обучение и функции SAAS, такие как Apollo Studio и управляемая федерация.
  • Другие более мелкие игроки, такие как hasura.io, stepzen, также предлагают SaaS-решения для GraphQL.

Умножение предложений затрудняет навигацию в экосистеме. Мне также было трудно получить четкие указания. Некоторые участники не согласны с будущим GraphQL. Одним интересным моментом разногласий является Stitching vs Federation.

С другой стороны, React, еще один проект с открытым исходным кодом, полностью поддерживается Facebook. Это облегчает наличие четких указаний. Когда Facebook решил перейти от классовых компонентов к хукам, они ясно дали понять. Я предпочитаю экосистему, которая сходится к общему решению.

Заключение

REST был новым SOAP; теперь GraphQL — это новый REST. История повторяется. Трудно сказать, является ли GraphQL просто новой крутой тенденцией, которая исчезнет, ​​или же она действительно изменит правила игры. Одно можно сказать наверняка, это все еще находится на ранней стадии и еще не убедило нашу команду.

Наши мобильные и клиентские команды в Spendesk любят GraphQL. Удивительные инструменты, возможность легко изучить API и строгая типизация (особенно с Kotlin и Swift) делают работу разработчиков более удобной. Это имеет смысл, если подумать об изначальной проблеме Facebook. GraphQL был разработан для решения мобильных проблем. Однако эти преимущества могут не иметь отношения к вашему проекту.

Большинство проблем возникает, когда вы начинаете разговаривать с бэкенд-инженерами. Отсутствие надлежащей нумерации страниц, кэширования и типов MIME — настоящие проблемы. Управление типами GraphQL и вашими собственными типами становится сложным, а распознаватель сложнее поддерживать. По мере роста вашего проекта вам придется инвестировать в очень дорогие инструменты, такие как Stitching или Federation, для исправления больших схем. Наконец, я считаю, что экосистема недостаточно зрелая, а стоимость поддержки этого решения слишком высока по сравнению с REST API.

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