Приятного чтения!
Если в последние 10 лет вам доводилось потреблять API – готов поспорить, что это был REST API. Вероятно, данные были структурированы вокруг ресурсов, в отклики включались id, указывающие на связанные объекты, а при помощи HTTP-команд сообщалось, как поступить с информацией: прочитать, записать и обновить (да, согласен, это вольное определение, а не канонический REST Роя Филдинга). Некоторое время API в стиле REST были доминирующим стандартом в нашей индустрии.
Однако, у REST есть свои проблемы. Клиент может привыкнуть извлекать лишние данные, запрашивая целый ресурс в случае, когда ему нужны лишь один-два фрагмента информации. Либо клиенту могут регулярно требоваться несколько объектов одновременно, но он не может извлечь их все в одном запросе – тогда возникает так называемое «недоизвлечение» данных. Что касается поддержки, изменения в REST API могут приводить к тому, что клиенту потребуется обновить всю интеграцию, чтобы программа соответствовала новой структуре API или схемам откликов.
Для решения подобных проблем в последние годы все активнее разрабатываются принципиально иные API, именуемые «графовыми».
Что такое Graph API?
Упрощенное определение графового API: это API, моделирующий данные в терминах узлов и ребер (объектов и отношений) и позволяющий клиенту взаимодействовать сразу со многими узлами в рамках единственного запроса. Допустим, на сервере содержатся данные об авторах, постах в блогах и комментариях к ним. Если у нас REST API, то для получения автора и комментариев к конкретному посту с клиента, возможно, потребуется сделать три HTTP-запроса, например:
/posts/123
, /authors/455
, /posts/123/comments
.В графовом API клиент формулирует вызов таким образом, что данные со всех трех ресурсов вытягиваются в один заход. Клиент также может указать те поля, которые для него действительно важны, предоставив более полный контроль над схемой отклика.
Чтобы детально исследовать, как устроен этот механизм, рассмотрим пару кейсов с описанием живых невыдуманных API.
Кейс 1: Графовый API Facebook
Facebook выпустил версию 1.0 своего API в 2010 году и с тех пор проектирует новые версии, вдохновляясь примером графовых баз данных. Существуют узлы, соответствующие, например, постам и комментариям, а также ребра, соединяющие их и указывающие, что данный комментарий «относится» к этому посту. Такой подход обеспечивает всей конструкции не менее качественную обнаружимость, чем у типичного REST API, однако, все равно позволяет клиенту оптимизировать извлечение данных. Возьмем в качестве примера отдельный пост и рассмотрим, какие простые операции можно с ним проделать.
Для начала клиент при помощи запроса GET выбирает пост из корня API, исходя из ID поста.
GET /<post-id>
По умолчанию в таком случае возвращается большинство полей верхнего уровня данного поста. Если клиенту требуется доступ лишь к некоторым элементам поста – например, заголовку и времени создания – то можно запросить только эти поля, указав данную информацию в качестве одного из параметров запроса:
GET /<post-id>?fields=caption,created_time
Чтобы выбрать требуемые данные, клиент запрашивает ребро, например, комментарии к посту:
GET /<post-id>/comments
До сих пор все это напоминает функции REST API. Пожалуй, возможность задать подмножество полей – в новинку, но в целом данные воспринимаются во многом как ресурсы. Ситуация становится интереснее, когда клиент собирает вложенный запрос. Вот как еще клиент может выбрать комментарии к посту:
GET /<post-id>?fields=caption,created_time,comments{id,message}
Вышеприведенный запрос возвращает отклик, в котором содержится время создания поста, его заголовок и список комментариев (из каждого сообщения выбирается только id и сообщение). В REST вы бы такое сделать не смогли. Клиенту потребовалось бы сначала выбрать пост, а затем — комментарии.
А что, если клиенту потребуются более глубокие вложения?
GET /<post-id>?fields=caption,created_time,comments{id,message,from{id,name}}
В этом запросе выбираются комментарии к посту, в том числе, id и имя автора каждого комментария. Рассмотрим, как это делалось бы в REST. Клиенту потребовалось бы запросить пост, запросить комментарии, а затем в серии отдельных запросов извлечь информацию об авторе каждого комментария. Сразу набирается множество HTTP-вызовов! Однако, при проектировании в виде графа вся эта информация конденсируется в одном вызове, и в этом вызове оказывается лишь та информация, что нужна клиенту.
Наконец, последний момент, который следует отметить о графовом проектировании: любой объект, выбираемый с ребра, сам является узлом и, следовательно, его можно запросить непосредственно. Вот, например, как выбирается дополнительная информация о конкретном комментарии:
GET /<comment-id>
Обратите внимание: клиенту не нужно собирать URL вида
/posts/<post-id>/comments/<comment-id>
, как могло бы потребоваться при работе с REST API. Это может пригодиться в ситуациях, когда у клиента нет непосредственного доступа к id родительского объекта.Такая же ситуация возникает при изменении данных. Например, если нам надо обновить и/или удалить объект (скажем, комментарий), применяется запрос PUT или DELETE соответственно, посылаемый непосредственно на конечную точку
id
. Чтобы создать объект, клиент может направить POST к соответствующему ребру узла. Так, чтобы добавить комментарий к посту, клиент делает запрос POST к ребру с комментариями от этого поста:POST /<post-id>/comments
message=This+is+a+comment
Кейс 2: GitHub V4 GraphQL API
Другим конкурентом графового API можно считать спецификацию под названием GraphQL. Эта концепция значительно отличается от REST, здесь предоставляется всего одна конечная точка, принимающая запросы GET и POST. При всех взаимодействиях с API отправляются запросы, соответствующие синтаксису GraphQL.
В мае 2017 года GitHub выпустил 4-ю версию своего API, соответствующую этой спецификации. Чтобы попробовать, каков из себя GraphQL, давайте рассмотрим отдельные операции, которые можно проделать с репозиторием.
Чтобы выбрать репозиторий, клиент определяет запрос GraphQL:
POST /graphql
{
"query": "repository(owner:\"zapier\", name:\"transformer\") {
id
description
}"
}
В данном запросе выбирается ID и описание репозитория “transformer” с ресурса Zapier org. Здесь следует отметить несколько вещей. Во-первых, мы считываем данные с API при помощи POST, поскольку посылаем в запросе тело сообщения. Во-вторых, полезная нагрузка самого запроса записана в формате JSON, что предписано в стандарте GraphQL. В-третьих, структура запроса будет именно такой, какая указана в нашем запросе,
{"data": {"repository": {"id": "MDEwOlJlcG9zaXRvcnk1MDEzODA0MQ==", "description": "..."}}}
(корневой ключ data
– еще один обязательный элемент, который должен присутствовать в откликах GraphQL).Чтобы выбрать данные, относящиеся к репозиторию – например, задачи и их авторов, клиент применяет вложенный запрос:
POST /graphql
{
"query": "repository(owner: \"zapier\", name: \"transformer\") {
id
description
issues(last: 20, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
title
body
author {
login
}
}
}
}"
}
Этот запрос выхватывает ID и описание репозитория, название и текст последних 20 задач, созданных в репозитории, а также логин (имя) автора каждой задачи. То есть, в каждом запросе укладывается масса информации. Вообразите, как выглядел бы REST-эквивалент такого запроса – и становится понятно, какие возможности и гибкость обеспечивает клиентам GraphQL в данном отношении.
При обновлении данных GraphQL использует концепцию под названием «мутация». В отличие от REST, где обновление выполняется путем PUT или POST измененной копии ресурса на ту же конечную точку, с которой клиент ее извлек, мутация GraphQL – это явная операция, определяемая API. Если клиенту требуется подкорректировать данные, то требуется знать, какие мутации поддерживаются на сервере. Удобно, что GraphQL позволяет обнаруживать их в рамках процесса под названием «интроспекция схемы».
Прежде, чем обсудить, что такое «интроспекция», нужно прояснить термин «схема». В GraphQL каждый API определяет набор типов, используемых при валидации запросов. До сих пор в GitHub мы работали с типами
repository
, issue
и author
. Каждый тип описывает данные, которые в нем содержатся, а также взаимосвязи этого типа с другими. В совокупности все эти типы образуют схему API.При наличии подробной схемы GraphQL в обязательном порядке требует, чтобы клиент имел возможность запрашивать эту схему в соответствии с синтаксисом GraphQL. Таким образом клиент может узнать возможности API путем интроспекции.
Если клиенту требуется узнать, какие мутации возможны в GitHub, можно просто запросить:
POST /graphql
{
"query": "__type(name: \"Mutation\") {
name
kind
description
fields {
name
description
}
}"
}
Среди мутаций, перечисленных в отклике, находим, например,
addStar
, позволяющую клиенту проставить звездочку репозиторию (или любому рейтингуемому объекту). Чтобы осуществить мутацию, используется подобный запрос:POST /graphql
{
"query": "mutation {
addStar(input:{starrableId:\"MDEwOlJlcG9zaXRvcnk1MDEzODA0MQ==\"}) {
starrable {
viewerHasStarred
}
}
}"
}
В этом запросе указано, что клиент собирается применить мутацию
addStar
и предоставляет аргументы, необходимые для выполнения такой операции; в данном случае, это лишь ID репозитория. Обратите внимание: в данном запросе в качестве префикса запроса используется ключевое слово mutation. Так GraphQL узнает, что клиент собирается выполнить мутацию. Во всех предыдущих запросах в качестве префикса также можно было поставить ключевое слово query, но его принято использовать, если тип операции не указан. Наконец, необходимо отметить, что клиент полностью контролирует данные, содержащиеся в отклике. В данном запросе клиент требует из репозитория поле viewerHasStarred
– в данном сценарии оно нас не слишком интересует, поскольку при мутации добавляется звездочка, и мы знаем, что она вернет true
. Однако, если клиент совершил иную мутацию – скажем, создал задачу, то может получить в ответ сгенерированные значения, например, ID или номер задачи, а также вложенные данные, например, общее количество открытых задач в данном репозитории.API будущего
Надеюсь, эти кейсы наглядно демонстрируют, как развивается дизайн API в SaaS-индустрии. Я не пытаюсь сказать, что за графовыми API будущее, а REST мертв. В таких архитектурах как GraphQL есть собственные проблемы. Но хорошо, что круг возможностей ширится, и в следующий раз, когда вам потребуется создать API, вы сможете взвесить все компромиссы, на которые приходится идти при том или ином варианте дизайна, и выбрать оптимальное решение.
Комментарии (9)
rznELVIS
17.02.2018 13:51Мне кажется, что классический REST отлично жил бы с протоколом HTTP/2 (https://habrahabr.ru/post/308846/). Новый протокол бы решил главную проблему REST — большое число запросов, т.к. позволил бы посылать их в одном TCP- соединение. Но так как HTTP/2 задерживается, то и начали появляться Odata, GraphQL и прочее. Это всё Отличные инструменты, но новый протокол бы был очень кстати. Все-так HTTP1.1 уже скоро 20 лет будет.
nsinreal
17.02.2018 14:48Этого недостаточно. Как минимум, при REST появляются запросы зависязие от результата предыдущего запроса (например, для вложенности третьего уровня).
И самое важное: при graphql у вас гибче сервер, что упрощает разработку клиента.TyVik
17.02.2018 22:41А почему тогда сразу не указывать все запрашиваемые поля объекта? Например, /users//?fields=id,name,groups.name,groups.founder.name. Получить также список групп и у каждой группы имя фаундера. Подобный обработчик для DjangoRestFramework я написал года 4 назад и с тех пор ни разу не заглядывал в это место, т.к. не было необходимости.
Другое дело если мы запрашиваем несвязанные объекты. Тут увы, концепция REST никак не ложится.nsinreal
17.02.2018 23:44У этого решения тоже есть свои недостатки:
— трудности с переносом в другой проект;
— на мелких проектах на любые велосипеды не хватит времени;
— не очень широкие возможности по фильтрации (а если вам нужны только группы удовлетворяющие определенному условию?);
— никто не сказал, что у вас хватит времени на корректную имплементацию этого добра (например, так чтобы вы загружали только запрошенные данные, но при этом еще и работал бекендский кеш)
Но как бы можно, да.
rznELVIS
17.02.2018 23:34Это-то да. Но вам придется переписывать API под graphQL. А если бы появился HTTP/2, то вам вообще ничего не надо делать. Запросы как летели так и будут лететь, а там пусть за них сервер отвечает. Для legacy очень хорошо. А таких проектов как всегда очень много
third112
Очень привлекательный подход. Граф очень универсальная структура, которая дает много возможностей при решении разнообразных задач.
А вот про проблемы хотелось бы услышать подробнее. В частности, для графов предложено много универсальных алгоритмов, однако в зависимости от типа графа алгоритмы часто видоизменяют. Даже представления графов на практике бывают различными — см., например.