Как стать автором
Обновить
140.88
hh.ru
HR Digital

GraphQL, что ты такое?

Время на прочтение7 мин
Количество просмотров16K

Привет! Сегодня у нашей статьи два автора — бэкенд-разработчик Артём и фронтенд-разработчик Илья. 

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

Если лень читать или больше нравится видеоформат — вам сюда

Разбираемся на пальцах

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

Мы разрабатываем отдельный от основного hh.ru продукт — внешнюю CRM-систему по подбору персонала Talantix. Если hh.ru, как и ряд схожих продуктов, это скорее job-борда, где кандидат создает резюме, а работодатель создает вакансию, на которую можно откликнуться, то после этого обычно в бой вступает CRM-система, где создаются интервью-встречи, кандидаты переводятся по кастомным этапам, строится аналитика работы рекрутеров и происходит прочий очень занимательный для hr флоу работы.

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

Итак, мы зашли на сайт и собираемся создать встречу на интервью.

Помимо всего прочего нам понадобятся ФИО и контакты кандидатов. Сделаем стандартный по REST GET-метод /candidates , который будет возвращать необходимые данные.

GET /candidates
[
  {
		"id": 1,
		"firstName": "Леонид",
		"lastName": "Якубович",
		"email": "leonid@yakubovich.ru",
		"phone": "+79672222222",
		"resume_id": 500
	},
	{
		"id": 2,
		"firstName": "Лёня",
		"lastName": "Агутин",
		"email": "hophey@lalaley.com",
		"phone": "+78005553535",
		"resume_id": 501
	}
]

Underfetching

Однако в нашей системе данные о кандидатах нужны не только на странице создания встречи. Ещё есть отдельная страница кандидатов, например, где помимо ФИО нам также понадобятся последний опыт работы и зарплата из резюме.

 

Окей, можем сделать ещё один GET-метод по REST, назовём его /resumes и по id резюме получим необходимые данные. 

GET /resumes?resumeId=500&resumeId=501
[
  {
		"resumeId": 500,
		"lastExperience": {
			"company": "Поле чудес",
			"position": "Ведущий"
		},
		"salary": {
			"currency": "RUB",
			"position": "45000"
		}
	},
	{
		"resumeId": 501,
		"lastExperience": {
			"company": "Голос",
			"position": "Ведущий"
		},
		"salary": {
			"currency": "RUB",
			"position": "50000"
		}
	}
]

Круто, но тогда появляется особенность, которая называется underfetching — мы не можем получить все данные за один запрос и ходим на бэкенд ещё раз. И это действительно иногда может стать проблемой — мы делаем несколько сетевых походов для получения данных на одной странице, не у всех пользователей может быть стабильный интернет, мы теряем в производительности.

Overfetching

Ну что же, мы можем забить на REST, создать единый метод /candidates, который будет возвращать все данные из сущностей кандидатов и резюме. Мы же знаем как и где они будут использоваться, так чего бы в один метод не напихать туда всё, что нам нужно?

GET /candidates
 [
   {
  		"id": 1,
  		"firstName": "Леонид",
  		"lastName": "Якубович",
  		"email": "leonid@yakubovich.ru",
  		"phone": "+79672222222",
  		"resume": {
  			"resumeId": 500,
  			"lastExperience": {
  				"company": "Поле чудес",
  				"position": "Ведущий"
  			},
  			"salary": {
  				"currency": "RUB",
  				"position": "45000"
  			}
  		}
  	},
  	{
  		"id": 2,
  		"firstName": "Лёня",
  		"lastName": "Агутин",
  		"email": "hophey@lalaley.com",
  		"phone": "+78005553535",
  		"resume": {
  			"resumeId": 501,
  			"lastExperience": {
  				"company": "Голос",
  				"position": "Ведущий"
  			},
  			"salary": {
  				"currency": "RUB",
  				"position": "50000"
  			}
  		}
  	}
  ]

Не всё так просто. При создании встречи нам не нужны данные о резюме. Эта проблема тоже имеет значение, и она называется overfetching. Мы отдаём избыточные данные, которые никак не будут использоваться – снова теряем в производительности.

Завязка на страницы приложения

Можно, например, создать методы /candidates_min и /candidates_max. С точки зрения производительности всё ок, но не очень масштабируемо - а если добавится третий метод, как назовём?

Но у нас есть знание какие данные нужны конкретным страницам - можно им и воспользоваться.

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

Клиент сам решает какие данные нужны

Есть ещё один вариант — оставить метод /candidates и в query-параметрах передавать только те поля, которые нам нужны. 

/candidates?fetchEmail=true&fetchPhone=true&fetchResumeSalary=true…

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

Да кто такой этот ваш GraphQL

GraphQL позволяет декларативно на клиенте описать, какие данные ему нужны, и бэкенд вернет только их. Это значит, что клиент сам говорит, какие данные он хочет получить. Например, как в sql-запросах, когда работаем с таблицами: перечисляем нужные поля и откуда их взять.  А со стороны бэкенда — это составление некой схемы, которой будут следовать запросы с клиента (опять же, аналог составления таблицы в sql), и разработка неких распознавателей, которые будут понимать, куда ходить за данными и что с ними делать. То есть на каждое поле объекта можно написать свой метод распознавателя, в котором будет содержаться его бизнес-логика.

GraphQL часто путают с базой данных или SQL, хотя мы только что убедились, что это совсем другой уровень абстракции. Более того, у нас теперь будет только один endpoint — POST-метод, который займется обслуживанием вообще всех запросов GraphQL, в отличие от того же REST. А значит, GraphQL — это стандарт языка описания запросов от клиента к бэкенду.

Реализация

Опишем краткую схему для нашего случая и заодно поймем, где граф в GraphQL.

Схема описывается на специальном языке GraphQL-схемы, и он чем-то похож на JSON. Мы описали кандидата, резюме и соединили их вместе. А вот и Graph в GraphQL — мы объявили ноды графа и соединили их. 

Однако войти в этот граф пока нельзя. Поэтому опишем входные точки. В данном случае будет одна точка — candidates, например, с фильтром по id. 

candidates(id: 5) {
  firstName,
  lastName,
  phone,
  email,
  resumes {
    lastExperience {
     company,
     position
   },
    salary {
      currency,
      position
    }
  }
}

И вот почему GraphQL — это QL (query language): запрос очень похож на JSON, но на самом деле это не он. И после этого сервер возвращает нам нужные данные.

Сравним с REST

В REST:

  • N ресурсов;

  • Бэкенд решает, какие данные выдать;

  • Нет строго контракта, вернее, контракт на уровне согласования;

  • Обработка ошибок через коды ответа 4**-5**.


В GraphQL:

  • 1 POST-ресурс;

  • Клиент решает, какие данные запросить;

  • Контракт диктуется схемой и имеет строгую типизацию;

  • Обработка ошибок через массив errors в теле ответа — это обусловлено тем, что graphQL не завязан на определённый протокол.

Таблица

REST

GraphQL

N ресурсов

1 POST-ресурс

Бэкенд решает, какие данные выдать

Клиент решает, какие данные запросить

Нет строгого контракта

Контракт диктуется схемой

Обработка ошибок через коды ответа

Обработка ошибок через массив errors в теле ответа

Из грязи в графы или как мы совершили переход

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

В промежуточных стадиях у нас могут быть 2 версии однотипного кода (legacy и graphQL), пока полностью не переведём целую сущность — это нормально, если в коде сразу объявить deprecated legacy-методы и не забивать с переводом остальной функциональности. В hh.ru рекомендуется делать 30% техналога на команду и мы воспользовались этой возможностью. Также мы ставили себе технические цели в OKR, чтобы лучше мониторить прогресс.

Важное допущение — мы переводим пока что только GET-методы, хоть и graphQL позволяет изменять сущности. Это сделано только потому, что пока что перевод GET-методов для нас более актуален. 

Плюсы

Мы сидим на GraphQL с осени прошлого года и продолжаем развивать этот протокол. Вот какие преимущества нам удалось обнаружить за это время:

  • Декларативное общение клиент-сервера и решение underfetching/overfetching проблем.

  • Единый API, который планируем расшарить не только для web-клиента, но и на пользователей.

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

  • Бэкенд работает по строго типизированной схеме — и, как следствие, получаем документированное API из коробки.

  • Поменяли подход к проектировании задачи — теперь мы проектируем API, а не страницы.

Гарольд рад

В чем подвох

Также мы встретились и с некоторыми трудностями.

  • Клиент теперь сам готовит данные. Мы говорили, что это хорошо, но есть нюанс. Клиент может запросить вообще все данные в любом количестве и бэкенд тогда слегка приляжет. Поэтому нужно уметь контролировать внешние запросы. И у GraphQL есть решение — мы можем навешивать веса на определенные поля, и если сумма весов превысила заданную константу, GraphQL зареджектит этот запрос. Этот подход нормально работает, но не всегда получается сделать гибко и удобно.

  • Экосистема Java. Всё не так плохо, но java-мир скорее на догоняющей позиции. Обсуждение java-фреймворков мы решили вынести в отдельную статью, а видео уже можно посмотреть тут.

  • Построение инфраструктуры. Мониторинг, обработка ошибок, встраивание в текущую инфру — всё это реально, но во всё это нужно вкладываться. По внедрению инфраструктуры graphQL на фронтенде можно посмотреть тут.

  • Редиректы теперь намного сложнее

  • Кеширование запросов непонятно как реализовать

  • Ретраи — не очень понятно, что теперь идемпотентный запрос, а что нет, и какие запросы можно ретраить

  • Работа с неструктурированными данными — graphQL не совсем предназначен для заливки и выдачи файлов, например

Гарольд не очень рад

В общем, минусов тоже хватает, поэтому мы бы не советовали рассматривать GraphQL как следующую ступень эволюции работы с API после REST. Хотя, если вы начнёте гуглить про graphQL, то увидите пёстрые статьи о том, как он "уничтожает REST целиком и полностью". Этому не стоит верить. GraphQL — “всего лишь” альтернатива REST со своими болячками. Просто посчитайте, даёт ли он вам больше плюсов, чем минусов, оцените риски и только после этого принимайте решение — внедрять его или нет. Мы внедрили и нам понравилось.

Заключение

Вот мы и поговорили в общих чертах про GraphQL, как мы его внедряли и на какие проблемы напоролись. Эта статья — только начало весёлого цикла про GraphQL. Не переключайтесь!

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

Теги:
Хабы:
Всего голосов 12: ↑9 и ↓3+6
Комментарии22

Публикации

Информация

Сайт
hh.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия