Pull to refresh

Покойся с миром, REST. Долгих лет жизни GraphQL

Reading time 13 min
Views 47K


Перевод. Автор оригинала Samer Buna. Оригинал статьи.

Когда я впервые узнал о GraphQL после долгого использования различных REST API, то не мог удержаться от твитов такого содержания:

Rest API превратился в REST-in-Peace API. Долгих лет жизни GraphQL


Примечание переводчика – Rest In Peace, RIP – распространенная эпитафия "Покойся с миром". Первое слово в ней пишется так же, как акроним REST.

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


Поймите правильно. Я не собираюсь обвинять GraphQL в убийстве REST или чём-то таком. REST не умрет никогда, также как XML будет жить вечно. Но кто в здравом уме станет использовать XML вместо JSON? На мой взгляд, GraphQL сделает для REST то же самое, что JSON сделал для XML.


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


Я поклонник подхода Начните с вопроса ЗАЧЕМ, поэтому так и поступим.


Вкратце: Зачем GraphQL?


Вот три наиболее важные проблемы, которые решает GraphQL:


  • Необходимость несколько раз обращаться за данными для рендеринга компонента. GraphQL позволяет получить все необходимые данные за один запрос к серверу.


  • Зависимость клиента от сервера. С помощью GraphQL клиент общается на универсальном языке запросов, который: 1) отменяет необходимость для сервера жестко задавать структуру или состав возвращаемых данных и 2) не привязывает клиента к конкретному серверу.


  • Неэффективные способы разработки. На GraphQL разработчики описывают необходимые для интерфейса данные с помощью декларативного языка. Разработчики сосредоточены на том, что хотят получить, а не как это сделать. Данные, необходимые для UI, тесно связаны с тем, как эти же данные описываются в GraphQL.

Статья подробно рассказывает, как GraphQL решает эти проблемы.


Начнем с простого описания для тех, кто еще не знаком с GraphQL.


Чем является GraphQL?


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


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


Чтобы сервис понимал GraphQL, нужно создать отдельный уровень в стеке обработки запросов и открыть к нему доступ клиентам, которым требуется взаимодействие с сервисом. Этот уровень можно считать транслятором языка GraphQL, или GraphQL-понимающим посредником. GraphQL не является платформой хранения данных. Поэтому нам не достаточно просто понимать синтаксис GraphQL, нужно еще транслировать запросы в данные.


Уровень GraphQL, написанный на любом языке программирования, содержит схему (schema) на подобии графа или диаграммы связей, из которой клиенты узнают о возможностях сервиса. Клиентские приложения, знакомые с GraphQL, могут делать запросы по этой схеме в соответствии со своими собственными возможностями. Такой подход отделяет клиентов от сервера и позволяет им развиваться и масштабироваться независимо.


Запросы на языке GraphQL могут быть либо запросами данных – query (операция чтения), либо мутациями – mutation (операции записи). В обоих случаях запрос это обычная строка, которую GraphQL-сервис разбирает, выполняет и сопоставляет с данными в определенном формате. Распространенный формат ответа для мобильных и веб-приложений – JSON.


Чем является GraphQL? (объяснение на пальцах)


GraphQL не выходит за рамки обмена данными. Существует клиент и сервер, которым надо взаимодействовать. Клиент должен сообщить серверу, какие данные нужны, а сервер – закрыть эту потребность актуальными данными. GraphQL находится в середине взаимодействия.


image


Скриншот из моего обучающего курса на Pluralsight – Building Scalable APIs with GraphQL


Вы спросите, почему клиент не может общаться с сервером напрямую? Конечно, может.


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


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


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


image


Скриншот из моего обучающего курса на Pluralsight – Building Scalable APIs with GraphQL


Вместо того чтобы напрямую обращаться к двум сервисам (на предыдущем слайде), клиент взаимодействует с уровнем GraphQL, и уже тот получает данные у сервисов. Таким образом, GraphQL избавляет клиента от необходимости поддерживать разные API, преобразуя единственный запрос клиента в запросы к нескольким поставщикам данных на понятном им языке.


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


Но компьютеры пока не столь умны, чтобы самостоятельно отвечать на произвольные вопросы, и где-то должны существовать алгоритмы. Вот почему на уровне GraphQL необходимо задать схему (schema), которую смогут использовать клиенты.


Схема в основе своей – это документ о возможностях, перечисляющий все вопросы, которые клиент может адресовать уровню GraphQL. Использовать схему можно довольно гибко, поскольку речь идет о графе (graph) узлов. В сущности, схема ограничивает то, что можно запросить на уровне GraphQL.


Не совсем понятно? Давайте взглянем на GraphQL, как на замену REST API, чем он в действительности является. Позвольте ответить на вопрос, который вы, наверняка, сейчас задаете.


Что не так с REST API?


Большая проблема REST API в многочисленности точек назначения (endpoints). Это вынуждает клиентов делать много запросов, чтобы получить нужные данные.


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


REST API не предлагает клиенту язык запросов. Клиент не влияет на то, какие данные возвращает сервер. Просто нет языка, на котором клиент мог бы указать это. Точнее говоря, доступные клиенту средства влиять на сервер очень ограничены.


Например, точка назначения для операции READ позволяет сделать одно из двух:


  • GET /ResouceName – получить список записей;
  • GET /ResourceName/ResourceID – получить запись по ID.

Клиент не может указать, например, какие поля записи хочет взять у данного ресурса. Эта информация зашита в самом REST-сервисе, и он всегда вернет все предусмотренные поля независимо от того, какие из них нужны клиенту. В GraphQL эта проблема называется перевыгрузкой (over-fetching) информации, которая не требуется. Перевыгрузка напрасно нагружает сеть и расходует память на стороне клиента и сервера.


Еще одна значимая проблема REST API – версионирование. Необходимость поддерживать несколько версий означает новые точки назначения. Это влечет дополнительные трудности в использовании и поддержке API и может стать причиной дублирования кода на сервере.


GraphQL призван решить указанные задачи. Конечно, это не все проблемы REST API, но я не хочу углубляться в то, чем REST API является или не является. Скорее, говорю об общепринятом ресурсо-ориентированном подходе к API на основе точек назначения HTTP. Со временем каждый такой API превращается в мешанину обычных точек назначения, как предписывает REST, и специальных, добавленных по соображениям производительности. И тут альтернатива в виде GraphQL выглядит намного лучше.


Что стоит за магией GraphQL?


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


  • GraphQL-схема является строго типизированной. Чтобы создать схему, задают поля (fields) определенных типов (types). Эти типы могут быть примитивами или пользовательскими типами, но все в схеме типизировано. Развитая система типов открывает такие возможности, как интроспекция API, и позволяет создавать мощные инструменты для клиентской и серверной части.


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


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

Благодаря последнему пункту я лично верю в превосходство GraphQL.


Все это высокоуровневые концепции. Рассмотрим чуть подробнее.


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


Другая важная идея – наличие развитого языка запросов, благодаря которому клиент может работать с единственной точкой назначения. Без такого языка не было бы смысла ограничивать количество точек назначения. Нужен язык для описания настраиваемых запросов и возврата данных.


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


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


Это особенно актуально для мобильных клиентов, поскольку невозможно прямо указать им, какую версию API использовать. Установленное мобильное приложение может использовать старую версию API на протяжении многих лет. Для веб этой проблемы нет, поскольку можно просто заменить код веб-приложения на сервере. Но это намного сложнее для мобильных приложений.


Все еще не убедил? Что если сравнить GraphQL и REST на конкретном примере?


RESTful API против GraphQL API — Пример


Допустим, разрабатывается интерфейс на тему фильма «Звездные войны» и его персонажей.


Первое, что нужно создать, – простой визуальный компонент для показа информации о каком-то одном персонаже Звездных войн. Возьмем Дарта Вейдера, который регулярно появляется на протяжении всего фильма. Компонент будет показывать имя, дату рождения, название планеты и названия всех фильмов, в которых участвует персонаж.


Звучит легко, но мы имеем дело с тремя различными ресурсами: Person, Planet и Film. Они достаточно просто взаимосвязаны, так что любой угадает структуру данных. Объект Person принадлежит планете и сам владеет от одного до нескольких объектов Film.


Данные для первого компонента выглядят так:


{
  "data": {
    "person": {
      "name": "Darth Vader",
      "birthYear": "41.9BBY",
      "planet": {
        "name": "Tatooine"
      },
      "films": [
        { "title": "A New Hope" },
        { "title": "The Empire Strikes Back" },
        { "title": "Return of the Jedi" },
        { "title": "Revenge of the Sith" }
      ]
    }
  }
}

Пусть с сервера приходит именно такая структура. Тогда можно визуализировать данные в React таким образом:


// The Container Component:
<PersonProfile person={data.person} ></PersonProfile>

// The PersonProfile Component:
Name: {person.name}
Birth Year: {person.birthYear}
Planet: {person.planet.name}
Films: {person.films.map(film => film.title)}

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


Посмотрим, как получить эти данные через RESTful API.


Сначала получим информацию о персонаже по ID. Ожидается, что RESTful API предоставляет ее так:


GET - /people/{id}

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


Ответ JSON может выглядеть так:


{
  "name": "Darth Vader",
  "birthYear": "41.9BBY",
  "planetId": 1
  "filmIds": [1, 2, 3, 6],
  *** other information we do not need ***
}

Затем получаем название планеты:


GET - /planets/1

И затем получаем названия фильмов:


GET - /films/1
GET - /films/2
GET - /films/3
GET - /films/6

И только после шести запросов мы можем собрать ответы и обеспечить компонент необходимыми данными.


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


Вы можете сами попробовать и увидеть, что я имею в виду. У Звездных войн есть RESTful API по адресу http://swapi.co/. Сконструируйте объект данных персонажа. Ключи могут называться чуть иначе, но урлы ресурсов будут те же. Вам потребуется ровно шесть запросов. Более того, вы получите избыточную информацию, не нужную для компонента.


Конечно, это одна из возможных реализаций RESTful API для этих данных. Можно представить реализацию лучше, которая упрощает задачу. Например, если в API доступны вложенные ресурсы и сервер знает о взаимосвязи персонажа и фильмов, можно получить фильмы так:


GET - /people/{id}/films

Но простой RESTful API наверняка не имеет такой возможности, и придется просить бекенд-разработчиков создать для нас дополнительные точки назначения. Такова практика масштабирования RESTful API – приходится добавлять все новые точки назначения, чтобы соответствовать потребностям клиентов. Поддерживать эти точки довольно трудоемко.


Теперь взглянем на подход GraphQL. Сервер GraphQL по максимуму воплощает идею настраиваемой точки назначения, доводя идею до абсолюта. На сервере есть только одна точка назначения, и способ связи с ней не важен. Если обратиться по HTTP, то метод HTTP-запроса будет проигнорирован. Предположим, имеется точка назначения GraphQL, доступная на /graphql по HTTP.


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


GET or POST - /graphql?query={...}

Запрос GraphQL – это просто строка, в которой указаны все необходимые данные. И здесь становится видна сила декларативности.


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


{
  person(ID: ...) {
    name,
    birthYear,
    planet {
      name
    },
    films {
      title
    }
  }
}

Сравните, как описана потребность в данных на человеческом языке и на GraphQL. Описания настолько близки, насколько возможно. Также сравните запрос GraphQL и тот JSON, с которого мы начали. Запрос в точности повторяет структуру ожидаемого JSON, не включая значения. Если провести параллель между запросом и ответом, то запрос является ответом без данных.


Если ответ выглядит так:


Ближайшая к солнцу планета – Меркурий.

То вопрос можно представить тем же самым выражением, но без конкретного значения:


(Какая) ближайшая к солнцу планета?

Таким же сходство обладает запрос GraphQL. Если взять результирующий JSON, убрать все «ответы» (значения), то получим запрос GraphQL, подходящий на роль вопроса о данном JSON.


Теперь сравним запрос GraphQL c декларативным кодом React UI, в котором описаны данные. Все, что указано в запросе GraphQL, используется в UI, и все используемое в UI присутствует в запросе.


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


Если поменять местами части этой модели, она будет так же полезна. По запросу GraphQL легко представить, как используется ответ в UI, поскольку ответ имеет такую же «структуру», как запрос. Не нужно специально изучать ответ чтобы понять, как его использовать, и даже не нужна документация по API. Все внутри запроса.


У Звездных войн имеется GraphQL API по адресу. Попробуйте с его помощью получить данные о персонаже. Есть незначительные отличия, но в целом запрос для получения необходимых данных в этом API выглядит так (с Дартом Вейдером в качестве примера):


{
  person(personID: 4) {
    name,
    birthYear,
    homeworld {
      name
    },
    filmConnection {
      films {
        title
      }
    }
  }
}

Запрос вернет структуру очень близкую к потребностям визуального компонента. Но главное, все данные получены за один раз.


Плата за гибкость GraphQL


Идеальное решение – миф. Вместе с гибкостью GraphQL приходят некоторые проблемы и заботы.


Одна из угроз, которой открыт GraphQL, это сетевые атаки на исчерпание ресурсов (типа Denial of Service). Сервер GraphQL может быть атакован избыточно сложными запросами, потребляющими все ресурсы. Легко запросить данные с глубокой вложенностью (пользователь –> друзья –> друзья друзей … ) или применить синонимы полей, чтобы принудить сервер получать одни и те же данные много раз. Хотя подобные атаки происходят не только на GraphQL, при работе с GraphQL нужно иметь их в виду.


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


Если GraphQL API не публичный и предназначен для внутренних клиентов (мобильных или веб), можно использовать списки доступа и предварительно одобренные запросы. Клиенты требуют сервер выполнить такие запросы, указывая вместо запроса его идентификатор. Кажется, Facebook применяет такой подход.


Еще один вопрос при работе с GraphQL – идентификация и авторизация пользователей. Когда выполнять ее – перед, во время или после обработки запроса на GraphQL?


Чтобы ответить на этот вопрос, будем считать, что GraphQL это DSL (domain specific language – язык предметной области) поверх обычной логики получения данных на бекенде. Действительно, это просто дополнительный уровень между клиентом и сервисом данных (или несколькими сервисами).


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


В GraphQL сложнее кешировать данные на клиенте. С RESTful API это проще, поскольку он подобен словарю. Конкретный адрес возвращает конкретные данные. Можно непосредственно использовать адрес как ключ кеширования.


В GraphQL тоже можно использовать текст запроса в качестве ключа. Но этот подход не очень эффективен и может нарушить целостность данных. Результаты разных запросов могут пересекаться, и такое примитивное кеширование не подходит в этом случае.


Однако существует блестящее решение этой проблемы. Graph Query равно Graph Cache. Если нормализовать данные, которые сформировал GraphQL: превратить иерархическую структуру в плоский список записей и присвоить каждой записи уникальный идентификатор, – то можно легко кешировать записи по отдельности, вместо целого ответа.


Это не так просто реализовать. Существуют взаимозависимые записи, и приходится работать с зацикленными графами. Чтобы записать в кеш или прочитать из него, необходимо полностью разобрать запрос. Требуется дополнительный уровень логики для работы с кешем. Но этот метод намного превосходит кеширование на основе текста запроса. Фреймворк Relay.js реализует эту стратегию кеширования «из коробки».


Возможно, самая большая проблема при использовании GraphQL – так называемые N+1 SQL-запросы. В GraphQL поля запроса реализованы как обычные функции, отправляющие запросы к базе данных. Чтобы заполнить все поля данными, может потребоваться новый SQL-запрос на каждое поле.


В обычном RESTful API легко проанализировать, выявить и решить проблему N+1, просто улучшая сконструированные SQL-запросы. Но в GraphQL, который обрабатывает поля динамически, это сложнее. К счастью, Facebook одним из первых предложил решение этой проблемы – DataLoader.


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


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


...


Спасибо за внимание. Я также разработал онлайн-курсы на Pluralsight и Lynda. Самые последние курсы – Advanced React.jsAdvanced Node.js и Learning Full-stack JavaScript.


Я провожу онлайн- и офлайн-тренинги для групп по JavaScript, Node.js, React.js и GraphQL от начального до продвинутого уровня. Напишите мне, если ищете преподавателя (англ.). Если возникли вопросы по данной статье или другим моим статьям, меня можно найти в этом аккаунте слак (принимаются приглашения самому себе) и задать вопрос в комнате #questions.


Автор оригинала Samer Buna
Оригинал статьи

Tags:
Hubs:
+20
Comments 142
Comments Comments 142

Articles