Знакомство с графовыми API

https://zapier.com/engineering/graph-apis/
  • Перевод
Привет, Хабр! Мы не перестаем отслеживать тему проектирования API после того, как встретили в портфеле издательства «Manning» вот эту книгу. Сегодня мы решили опубликовать обзорную статью об относительно новых Graph API и предлагаем еще раз задуматься о том, каковы будут новые API после безраздельной популярности REST.

Приятного чтения!

Если в последние 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, вы сможете взвесить все компромиссы, на которые приходится идти при том или ином варианте дизайна, и выбрать оптимальное решение.

Издательский дом «Питер»

263,00

Компания

Поделиться публикацией

Похожие публикации

Комментарии 10
    0
    Очень привлекательный подход. Граф очень универсальная структура, которая дает много возможностей при решении разнообразных задач.

    В таких архитектурах как GraphQL есть собственные проблемы.


    А вот про проблемы хотелось бы услышать подробнее. В частности, для графов предложено много универсальных алгоритмов, однако в зависимости от типа графа алгоритмы часто видоизменяют. Даже представления графов на практике бывают различными — см., например.
      0
      А чем плохи граф ориентированные базы данных? Использую JanusGraph, очень доволен.
        0
        Ничем не плохи) В статье говорится об API, которое не зависит от БД.
        +1
        Мне кажется, что классический REST отлично жил бы с протоколом HTTP/2 (https://habrahabr.ru/post/308846/). Новый протокол бы решил главную проблему REST — большое число запросов, т.к. позволил бы посылать их в одном TCP- соединение. Но так как HTTP/2 задерживается, то и начали появляться Odata, GraphQL и прочее. Это всё Отличные инструменты, но новый протокол бы был очень кстати. Все-так HTTP1.1 уже скоро 20 лет будет.
          –1

          Этого недостаточно. Как минимум, при REST появляются запросы зависязие от результата предыдущего запроса (например, для вложенности третьего уровня).
          И самое важное: при graphql у вас гибче сервер, что упрощает разработку клиента.

            0
            А почему тогда сразу не указывать все запрашиваемые поля объекта? Например, /users//?fields=id,name,groups.name,groups.founder.name. Получить также список групп и у каждой группы имя фаундера. Подобный обработчик для DjangoRestFramework я написал года 4 назад и с тех пор ни разу не заглядывал в это место, т.к. не было необходимости.

            Другое дело если мы запрашиваем несвязанные объекты. Тут увы, концепция REST никак не ложится.
              –1
              У этого решения тоже есть свои недостатки:
              — трудности с переносом в другой проект;
              — на мелких проектах на любые велосипеды не хватит времени;
              — не очень широкие возможности по фильтрации (а если вам нужны только группы удовлетворяющие определенному условию?);
              — никто не сказал, что у вас хватит времени на корректную имплементацию этого добра (например, так чтобы вы загружали только запрошенные данные, но при этом еще и работал бекендский кеш)

              Но как бы можно, да.
              0
              Это-то да. Но вам придется переписывать API под graphQL. А если бы появился HTTP/2, то вам вообще ничего не надо делать. Запросы как летели так и будут лететь, а там пусть за них сервер отвечает. Для legacy очень хорошо. А таких проектов как всегда очень много
                0
                Да, каждому инструменту найдется свое применение :-)
            0
            А можно полслова про реализацию?
            GraphQL выглядит волшебно! Мне frontend мне уши про него прожужжал. Но, как его реализовывать на backend? Или, по сути, это всего лишь batch mode для чего-то вроде REST?
            На сколько это усложняет жизнь backend разработчиков?

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

            Самое читаемое