[Перевод] Подробности о GraphQL: что, как и почему
GraphQL сейчас, без преувеличения, это — последний писк IT-моды. И если вы пока не знаете о том, что это за технология, о том, как ей пользоваться, и о том, почему она может вам пригодиться, значит статья, перевод которой мы сегодня публикуем, написана специально для вас. Здесь мы разберём основы GraphQL на примере реализации схемы данных для API компании, которая занимается попкорном. В частности, поговорим о типах данных, запросах и мутациях.
Что такое GraphQL?
GraphQL — это язык запросов, используемый клиентскими приложениями для работы с данными. C GraphQL связано такое понятие, как «схема» — это то, что позволяет организовывать создание, чтение, обновление и удаление данных в вашем приложении (то есть — перед нами четыре базовые функции, используемые при работе с хранилищами данных, которые обычно обозначают акронимом CRUD — create, read, update, delete).
Выше было сказано, что GraphQL используется для работы с данными в «вашем приложении», а не «в вашей базе данных». Дело в том, что GraphQL — это система, независимая от источников данных, то есть, для организации её работы неважно — где именно хранятся данные.
Если взглянуть, ничего не зная о GraphQL, на название этой технологии, то может показаться, что перед нами что-то очень сложное и запутанное. В названии технологии имеется слово «Graph». Означает ли это, что для того, чтобы её освоить, придётся учиться работать с графовыми базами данных? А то, что в названии есть «QL» (что может значить «query language», то есть — «язык запросов»), означает ли, что тем, кто хочет пользоваться GraphQL, придётся осваивать совершенно новый язык программирования?
Эти страхи не вполне оправданы. Для того чтобы вас успокоить — вот жестокая правда об этой технологии: она представляет собой всего лишь приукрашенные GET
или POST
запросы. В то время как GraphQL, в целом, вводит некоторые новые концепции, касающиеся организации данных и взаимодействия с ними, внутренние механизмы этой технологии полагаются на старые добрые HTTP-запросы.
Переосмысление технологии REST
Гибкость — это то, что отличает технологию GraphQL от широко известной технологии REST. При использовании REST, если всё сделано правильно, конечные точки обычно создают с учётом особенностей некоего ресурса или типа данных приложения.
Например, при выполнении GET
-запроса к конечной точке /api/v1/flavors
ожидается, что она отправит ответ, выглядящий примерно так:
[
{
"id": 1,
"name": "The Lazy Person's Movie Theater",
"description": "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!"
}, {
"id": 2,
"name": "What's Wrong With You Caramel",
"description": "You're a crazy person that likes sweet popcorn. Congratulations."
}, {
"id": 3,
"name": "Gnarly Chili Lime",
"description": "The kind of popcorn you make when you need a good smack in the face."}
]
В таком ответе ничего катастрофически неправильного нет, но подумаем о пользовательском интерфейсе, или скорее о том, как мы намереваемся потреблять эти данные.
Если мы хотим вывести в интерфейсе простой список, который содержит лишь названия имеющихся видов попкорна (и ничего другого), то этот список может выглядеть так, как показано ниже.
Список видов попкорна
Видно, что тут мы попали в непростую ситуацию. Мы вполне можем решить не использовать поле description
, но собираемся ли мы сидеть сложа руки и делать вид, будто мы не отправляли это поле клиенту? А что нам ещё остаётся делать? А когда нас, через несколько месяцев, спросят о том, почему приложение так медленно работает у пользователей, нам останется лишь дать дёру и больше не встречаться с руководством компании, для которой мы сделали это приложение.
На самом деле, то, что сервер отправляет в ответ на запрос клиента ненужные данные, это не полностью наша вина. REST — это механизм получения данных, который можно сравнить с рестораном, в котором официант спрашивает посетителя: «Чего вы хотите?», и, не особенно обращая внимание на его пожелания, говорит ему: «Я принесу вам то, что у нас есть».
Если же отбросить в сторону шутки, то в реальных приложениях подобное может вести к проблемным ситуациям. Например, мы можем выводить различные дополнительные сведения о каждом виде попкорна, наподобие сведений о цене, информации о производителе или диетологических сведений («Веганский попкорн!»). При этом негибкие конечные точки REST сильно усложняют получение специфических данных о конкретных видах попкорна, что ведёт к неоправданно высокой нагрузке на системы и к тому, что получающиеся решения оказываются далеко не такими, которыми разработчики могли бы гордиться.
Как технология GraphQL улучшает то, для чего использовалась технология REST
При поверхностном анализе вышеописанной ситуации может показаться, что перед нами всего лишь незначительная проблема. «Что плохого в том, что мы отправляем клиенту ненужные данные?». Для того чтобы понять масштабы, в которых «ненужные данные» могут стать большой проблемой, вспомним о том, что технология GraphQL была разработана компанией Facebook. Этой компании приходится обслуживать миллионы запросов в секунду.
Что это значит? А то, что при таких объёмах значение имеет каждая мелочь.
GraphQL, если продолжить аналогию с рестораном, вместо того, чтобы «нести» посетителю «то, что есть», приносит именно то, что посетитель заказывает.
Мы можем получить от GraphQL ответ, ориентированный на тот контекст, в котором используются данные. При этом нам не нужно добавлять в систему «одноразовые» точки доступа, выполнять множество запросов или писать многоэтажные условные конструкции.
Как работает GraphQL?
Как мы уже говорили, GraphQL, для передачи данных клиенту и получения их от него, полагается на простые GET
или POST
-запросы. Если подробнее рассмотреть эту мысль, то оказывается, что в GraphQL есть два вида запросов. К первому виду относятся запросы на чтение данных, которые в терминологии GraphQL называются просто запросами (query) и относятся к букве R (reading, чтение) акронима CRUD. Запросы второго вида — это запросы на изменение данных, которые в GraphQL называют мутациями (mutation). Они относятся к буксам C, U и D акронима CRUD, то есть — с их помощью выполняют операции создания (create), обновления (update) и удаления (delete) записей.
Все эти запросы и мутации отправляют на URL GraphQL-сервера, который, например, может выглядеть как https://myapp.com/graphql
, в виде GET
или POST
-запросов. Подробнее об этом мы поговорим ниже.
Запросы GraphQL
Запросы GraphQL — это сущности, представляющие собой запрос к серверу на получение неких данных. Например, у нас есть некий пользовательский интерфейс, который мы хотим заполнить данными. За этими данными мы и обращаемся к серверу, выполняя запрос. При использовании традиционных REST API наш запрос принимает вид GET-запроса. При работе с GraphQL используется новый синтаксис построения запросов:
{
flavors {
name
}
}
Это что, JSON? Или JavaScript-объект? Ни то и ни другое. Как мы уже говорили, в названии технологии GraphQL две последние буквы, QL, означают «query language», то есть — язык запросов. Речь идёт, в буквальном смысле, о новом языке написания запросов на получение данных. Звучит всё это как описание чего-то довольно сложного, но на самом деле ничего сложного тут нет. Разберём вышеприведённый запрос:
{
// Сюда помещают описания полей, которые нужно получить.
}
Все запросы начинаются с «корневого запроса», а то, что нужно получить в ходе выполнения запроса, называется полем. Для того чтобы избавить себя от путаницы, лучше всего называть эти сущности «полями запроса в схеме». Если вам такое наименование кажется непонятным — подождите немного — ниже мы подробнее поговорим о схеме. Здесь мы, в корневом запросе, запрашиваем поле flavors
.
{
flavors {
// Вложенные поля, которые мы хотим получить для каждого значения flavor.
}
}
Запрашивая некое поле, мы, кроме того, должны указать вложенные поля, которые нужно получить для каждого объекта, который приходит в ответе на запрос (даже если ожидается, что в ответ на запрос придёт всего один объект).
{
flavors {
name
}
}
Что в итоге получится? После того, как мы отправим такой запрос GraphQL-серверу, мы получим хорошо оформленный аккуратный ответ наподобие следующего:
{
"data": {
"flavors": [
{ "name": "The Lazy Person's Movie Theater" },
{ "name": "What's Wrong With You Caramel" },
{ "name": "Gnarly Chili Lime" }
]
}
}
Обратите внимание на то, что здесь нет ничего лишнего. Для того чтобы было понятнее — вот ещё один запрос, выполняемый для получения данных на другой странице приложения:
{
flavors {
id
name
description
}
}
В ответ на этот запрос мы получим следующее:
{
"data": {
"flavors": [
{ "id": 1, "name": "The Lazy Person's Movie Theater", description: "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!" },
{ "id": 2, "name": "What's Wrong With You Caramel", description: "You're a crazy person that likes sweet popcorn. Congratulations." },
{ "id": 3, "name": "Gnarly Chili Lime", description: "A friend told me this would taste good. It didn't. It burned my kernels. I haven't had the heart to tell him." }
]
}
}
Как видите, GraphQL — очень мощная технология. Обращаемся мы к одной и той же конечной точке, а ответы на запросы в точности соответствуют тому, что нужно для наполнения той страницы, с которой выполняются эти запросы.
Если нам нужно получить лишь один объект flavor
, то мы можем воспользоваться тем фактом, что GraphQL умеет работать с аргументами:
{
flavors(id: "1") {
id
name
description
}
}
Тут мы жёстко задали в коде конкретный идентификатор (id
) объекта, сведения о котором нам нужны, но в подобных случаях можно использовать и динамические идентификаторы:
query getFlavor($id: ID) {
flavors(id: $id) {
id
name
description
}
}
Здесь, в первой строке, мы даём запросу имя (имя выбирается произвольным образом, getFlavor
можно заменить на нечто вроде pizza
, и запрос останется работоспособным) и объявляем переменные, которые ожидает запрос. В данном случае предполагается, что запросу будет передан идентификатор (id
) скалярного типа ID
(о типах мы поговорим ниже).
Независимо от того, статический или динамический id
используется при выполнении запроса, вот как будет выглядеть ответ на подобный запрос:
{
"data": {
"flavors": [
{ "id": 1, "name": "The Lazy Person's Movie Theater", description: "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!" }
]
}
}
Как видите, всё устроено очень удобно. Вероятно, вы уже начинаете размышлять о применении GraphQL в собственном проекте. И, хотя то, о чём мы уже говорили, выглядит замечательно, красота GraphQL по-настоящему проявляется там, где работают с вложенными полями. Предположим, что в нашей схеме есть ещё одно поле, которое называется nutrition
и содержит сведения о пищевой ценности разных видов попкорна:
{
flavors {
id
name
nutrition {
calories
fat
sodium
}
}
}
Может показаться, что в нашем хранилище данных, в каждом объекте flavor
, будет содержаться вложенный объект nutrition
. Но это не совсем так. Используя GraphQL можно комбинировать обращения к самостоятельным, но связанным источникам данных в одном запросе, что позволяет получать ответы, дающие удобство работы с вложенными данными без необходимости денормализации базы данных:
{
"data": {
"flavors": [
{
"id": 1,
"name": "The Lazy Person's Movie Theater",
"nutrition": {
"calories": 500,
"fat": 12,
"sodium": 1000
}
},
...
]
}
}
Это способно значительно увеличить продуктивность труда программиста и скорость работы системы.
До сих пор мы говорили о запросах на чтение. А как насчёт запросов на обновление данных? Их использование даёт нам те же удобства?
Мутации GraphQL
В то время как запросы GraphQL выполняют загрузку данных, мутации ответственны за внесение в данные изменений. Мутации могут быть использованы в виде базового механизма RPC (Remote Procedure Call, вызов удалённых процедур) для решения различных задач наподобие отправки данных пользователя API стороннего разработчика.
При описании мутаций используется синтаксис, напоминающий тот, который мы применяли при формировании запросов:
mutation updateFlavor($id: ID!, $name: String, $description: String) {
updateFlavor(id: $id, name: $name, description: $description) {
id
name
description
}
}
Здесь мы объявляем мутацию updateFlavor
, указывая некоторые переменные — id
, name
и description
. Действуя по той же схеме, которая применяется при описании запросов, мы «оформляем» изменяемые поля (корневую мутацию) с помощью ключевого слова mutation
, за которым следует имя, описывающее мутацию, и набор переменных, которые нужны для формирования соответствующего запроса на изменение данных.
Эти переменные включают в себя то, что мы пытаемся изменить, или то, мутацию чего мы хотим вызвать. Обратите внимание также и на то, что после выполнения мутации мы можем запросить возврат некоторых полей.
В данном случае нам нужно получить, после изменения записи, поля id
, name
и description
. Это может пригодиться при разработке чего-то вроде оптимистичных интерфейсов, избавляя нас от необходимости выполнять запрос на получение изменённых данных после их изменения.
Разработка схемы и подключение её к GraphQL-серверу
До сих пор мы говорили о том, как GraphQL работает на клиенте, о том, как выполняют запросы. Теперь поговорим о том, как на эти запросы реагировать.
▍GraphQL-сервер
Для того, чтобы выполнить GraphQL-запрос, нужен GraphQL-сервер, которому можно такой запрос отправить. GraphQL-сервер представляет собой обычный HTTP-сервер (если вы пишете на JavaScript — то это может быть сервер, созданный с помощью Express или Hapi), к которому присоединена GraphQL-схема.
import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'
const app = express()
app.use('/graphql', graphqlHTTP({
schema: schema,
graphiql: true
}))
app.listen(4000)
Под «присоединением» схемы мы понимаем механизм, который пропускает через схему запросы, полученные от клиента, и возвращает ему ответы. Это похоже на воздушный фильтр, через который воздух поступает в помещение.
Процесс «фильтрации» связан с запросами или мутациями, отправляемыми клиентом на сервер. И запросы и мутации разрешаются с использованием функций, связанных с полями, определёнными в корневом запросе или в корневой мутации схемы.
Выше приведён пример каркаса HTTP-сервера, созданного с помощью JavaScript-библиотеки Express. Используя функцию graphqlHTTP
из пакета express-graphql
от Facebook, мы «прикрепляем» схему (предполагается, что она описана в отдельном файле) и запускаем сервер на порту 4000. То есть, клиенты, если говорить о локальном использовании этого сервера, смогут отправлять запросы по адресу http://localhost:4000/graphql
.
▍Типы данных и распознаватели
Для того чтобы обеспечить работу GraphQL-сервера, нужно подготовить схему и присоединить её к нему.
Вспомните о том, что выше мы говорили об объявлении полей в корневом запросе или в корневой мутации.
import gql from 'graphql-tag'
import mongodb from '/path/to/mongodb’ // Это - лишь пример. Предполагается, что `mongodb` даёт нам подключение к MongoDB.
const schema = {
typeDefs: gql`
type Nutrition {
flavorId: ID
calories: Int
fat: Int
sodium: Int
}
type Flavor {
id: ID
name: String
description: String
nutrition: Nutrition
}
type Query {
flavors(id: ID): [Flavor]
}
type Mutation {
updateFlavor(id: ID!, name: String, description: String): Flavor
}
`,
resolvers: {
Query: {
flavors: (parent, args) => {
// Предполагается, что args равно объекту, наподобие { id: '1' }
return mongodb.collection('flavors').find(args).toArray()
},
},
Mutation: {
updateFlavor: (parent, args) => {
// Предполагается, что args равно объекту наподобие { id: '1', name: 'Movie Theater Clone', description: 'Bring the movie theater taste home!' }
// Выполняем обновление.
mongodb.collection('flavors').update(args)
// Возвращаем flavor после обновления.
return mongodb.collection('flavors').findOne(args.id)
},
},
Flavor: {
nutrition: (parent) => {
return mongodb.collection('nutrition').findOne({
flavorId: parent.id,
})
}
},
},
}
export default schema
Определение полей в схеме GraphQL состоит из двух частей — из объявлений типов (typeDefs
) и распознавателей (resolver
). Сущность typeDefs
содержит объявления типов для данных, используемых в приложении. Например, ранее мы говорили о запросе на получение с сервера списка объектов flavor
. Для того чтобы к нашему серверу можно было бы выполнить подобный запрос, нужно сделать следующие три шага:
- Сообщить схеме о том, как выглядят данные объектов
flavor
(в примере, приведённом выше, это выглядит как объявление типаtype Flavor
). - Объявить поле в корневом поле
type Query
(это свойствоflavors
значенияtype Query
). - Объявить функцию-распознаватель объекта
resolvers.Query
, написанную в соответствии с полями, объявленными в корневом полеtype Query
.
Обратим теперь внимание на typeDefs
. Здесь мы сообщаем схеме сведения о форме (shape) наших данных. Другими словами, мы сообщаем GraphQL о разных свойствах, которые могут содержаться в сущностях соответствующего типа.
type Flavor {
id: ID
name: String
description: String
nutrition: Nutrition
}
Объявление type Flavor
указывает на то, что объект flavor
может содержать поле id
типа ID
, поле name
типа String
, поле description
типа String
и поле nutrition
типа Nutrition
.
В случае с nutrition
мы используем здесь имя другого типа, объявленного в typeDefs
. Здесь конструкция type Nutrition
описывает форму данных о пищевой ценности попкорна.
Обратите внимание на то, что мы тут, как и в самом начале этого материала, говорим о «приложении», а не о «базе данных». В вышеприведённом примере предполагается, что у нас есть база данных, но данные в приложение могут поступать из любого источника. Это может быть даже API стороннего разработчика или статический файл.
Так же, как мы поступали в объявлении type Flavor
, здесь мы указываем имена полей, которые будут содержаться в объектах nutrition
, используя, в качестве типов данных этих полей (свойств) то, что в GraphQL называется скалярными типами данных. На момент написания этого материала в GraphQL поддерживалось 5 встроенных скалярных типов данных:
Int
: целое 32-битное число со знаком.Float
: число двойной точности с плавающей точкой со знаком.String
: последовательность символов в кодировке UTF-8.Boolean
: логическое значениеtrue
илиfalse
.ID
: уникальный идентификатор, часто используемый для многократной загрузки объектов или в качестве ключа в кэше. Значения типаID
сериализуются так же, как строки, однако указание на то, что некое значение имеет типID
, подчёркивает тот факт, что это значение предназначено не для показа его людям, а для использования в программах.
В дополнение к этим скалярным типам мы можем назначать свойствам и типы, определённые нами самостоятельно. Именно так мы поступили, назначив свойству nutrition
, описанному в конструкции type Flavor
, тип Nutrition
.
type Query {
flavors(id: ID): [Flavor]
}
В конструкции type Query
, в которой описывается корневой тип Query
(тот «корневой запрос», о котором мы говорили ранее), мы объявляем имя поля, которое может быть запрошено. Объявляя это поле, мы, кроме того, вместе с типом данных, который ожидаем вернуть, указываем аргументы, которые могут поступить в запросе.
В данном примере мы ожидаем возможного поступления аргумента id
скалярного типа ID
. В качестве ответа на такой запрос ожидается массив объектов, устройство которых напоминает устройство типа Flavor
.
▍Подключение распознавателя запросов
Теперь, когда в корневом type Query
имеется определение поля field
, нам нужно описать то, что называется функцией-распознавателем.
Это — то место, где GraphQL, более или менее, «останавливается». Если мы посмотрим на объект resolvers
схемы, а затем на объект Query
, вложенный в него, мы можем увидеть там свойство flavors
, которому назначена функция. Эта функция и называется распознавателем для поля flavors
, которое объявлено в корневом type Query
.
typeDefs: gql`…`,
resolvers: {
Query: {
flavors: (parent, args) => {
// Предполагается, что args равно объекту наподобие { id: '1' }
return mongodb.collection('flavors').find(args).toArray()
},
},
…
},
Эта функция-распознаватель принимает несколько аргументов. Аргумент parent
— это родительский запрос, если таковой существует, аргумент args
тоже передаётся запросу в том случае, если он существует. Здесь ещё может использоваться аргумент context
, который в нашем случае не представлен. Он даёт возможность работать с различными «контекстными» данными (например — со сведениями о текущем пользователе в том случае, если сервер поддерживает систему учётных записей пользователей).
Внутри распознавателя мы делаем всё, что нужно для того, чтобы выполнить запрос. Именно здесь GraphQL «перестаёт беспокоиться» о происходящем и позволяет нам выполнять загрузку и возврат данных. Тут, повторимся, можно работать с любыми источниками данных.
Хотя GraphQL и не интересуют источники поступления данных, эту систему чрезвычайно сильно интересует то, что именно мы возвращаем. Мы можем вернуть JSON-объект, массив JSON-объектов, промис (его разрешение GraphQL берёт на себя).
Тут мы используем мок-обращение к коллекции flavors
базы данных MongoDB, передавая args
(если соответствующий аргумент передан распознавателю) в вызов .find()
и возвращая то, что будет найдено в результате выполнения этого вызова, в виде массива.
▍Получение данных для вложенных полей
Выше мы уже разобрали кое-что, относящееся к GraphQL, но сейчас, возможно, пока непонятно то, как быть с вложенным полем nutrition
. Помните о том, что данные, представленные полем Nutrition
, мы, на самом деле, не храним совместно с основными данными, описывающими сущность flavor
. Мы исходим из предположения о том, что эти данные хранятся в отдельной коллекции/таблице базы данных.
Хотя мы сообщили GraphQL о том, что type Flavor
может включать в себя данные nutrition
в форме type Nutrition
, мы не пояснили системе порядок получения этих данных из хранилища. Эти данные, как уже было сказано, хранятся отдельно от данных сущностей flavor
.
typeDefs: gql`
type Nutrition {
flavorId: ID
calories: Int
fat: Int
sodium: Int
}
type Flavor {
[…]
nutrition: Nutrition
}
type Query {…}
type Mutation {…}
`,
resolvers: {
Query: {
flavors: (parent, args) => {…},
},
Mutation: {…},
Flavor: {
nutrition: (parent) => {
return mongodb.collection('nutrition').findOne({
flavorId: parent.id,
})
}
},
},
Если присмотреться повнимательнее к объекту resolvers
в схеме, то можно заметить, что тут имеются вложенные объекты Query
, Mutation
и Flavor
. Они соответствуют типам, которые мы объявили выше в typeDefs
.
Если посмотреть на объект Flavors
, то окажется, что поле nutrition
в нём объявлено как функция-распознаватель. Заметной особенностью такого решения является тот факт, что мы объявляем функцию непосредственно в типе Flavor
. Другими словами, мы говорим системе: «Мы хотим, чтобы ты загрузила поле nutrition
именно так для любого запроса, использующего type Flavor
».
В этой функции мы выполняем обычный запрос к MongoDB, но тут обратите внимание на то, что мы используем аргумент parent
, переданный функции-распознавателю. То, что представлено здесь аргументом parent
, представляет собой то, что содержится в полях, имеющихся во flavors
. Например, если нам нужны все сущности flavor
, мы выполним такой запрос:
{
flavors {
id
name
nutrition {
calories
}
}
}
Каждое поле flavor
, возвращённое из flavors
, мы пропустим через распознаватель nutrition
, при этом данное значение будет представлено аргументом parent
. Если присмотреться к этой конструкции, то окажется, что мы, в запросе к MongoDB, используем поле parent.id
, которое представляет собой id
сущности flavor
, обработкой которой мы занимаемся в данный момент.
Идентификатор parent.id
передаётся в запросе к базе данных, где производится поиск записи nutrition
с идентификатором flavorId
, которая соответствует обрабатываемой сущности flavor
.
▍Подключение мутаций
То, что мы уже знаем о запросах, отлично переносится и на мутации. На самом деле, процесс подготовки мутаций практически полностью совпадает с процессом подготовки запросов. Если взглянуть на корневую сущность type Mutation
, то можно увидеть, что мы объявили в ней поле updateFlavor
, принимающее аргументы, задаваемые на клиенте.
type Mutation {
updateFlavor(id: ID!, name: String, description: String): Flavor
}
Этот код можно расшифровать так: «Мы ожидаем, что мутация updateFlavor
принимает id
типа ID
(восклицательный знак, !
, сообщает GraphQL о том, что это поле необходимо), name
типа String
и description
типа String
». Кроме того, после завершения выполнения мутации мы ожидаем возврат некоторых данных, структура которых напоминает тип Flavor
(то есть — объект, который содержит свойства id
, name
, description
, и, возможно, nutrition
).
{
typeDefs: gql`…`,
resolvers: {
Mutation: {
updateFlavor: (parent, args) => {
// Предполагается, что args равно объекту наподобие { id: '1', name: 'Movie Theater Clone', description: 'Bring the movie theater taste home!' }
// Выполняем обновление.
mongodb.collection('flavors').update(
{ id: args.id },
{
$set: {
...args,
},
},
)
// Возвращаем flavor после обновления.
return mongodb.collection('flavors').findOne(args.id)
},
},
},
}
Внутри функции-распознавателя для мутации updateFlavor
мы делаем именно то, чего от подобной функции можно ожидать: организуем взаимодействие с базой данных для того, чтобы изменить в ней, то есть — обновить, сведения об интересующей нас сущности flavor
.
Обратите внимание на то, что сразу же после выполнения обновления обновление мы выполняем обращение к базе данных для того, чтобы снова найти ту же сущность flavor
и вернуть её из распознавателя. Почему это так?
Вспомните о том, что на клиенте мы ожидаем получить объект в том состоянии, в которое он был приведён после завершения мутации. В данном примере мы ожидаем, что будет возвращена сущность flavor
, которую мы только что обновили.
Можно ли просто вернуть объект args
? Да, можно. Причина, по которой мы решили в данном случае этого не делать, заключается в том, что мы хотим быть на 100% уверенными в том, что операция обновления информации в базе данных прошла успешно. Если мы прочтём из базы данных информацию, которая должна быть изменённой, и окажется, что она и правда изменена, тогда можно сделать вывод о том, что операция выполнена успешно.
Зачем может понадобиться использовать GraphQL?
Хотя то, созданием чего мы только что занимались, выглядит не особенно масштабно, сейчас у нас есть функционирующее, хотя и простое, GraphQL-API.
Как и в случае с любой новой технологией, после первого знакомства с GraphQL вы можете задаться вопросом о том, зачем вам может пригодиться нечто подобное. Честно говоря, на этот вопрос нельзя дать однозначного и простого ответа. Очень уж много всего нужно учесть для того, чтобы такой ответ найти. И можно, кстати, подумать о том, чтобы вместо GraphQL просто выбрать проверенную временем технологию REST или напрямую обращаться к базе данных. Собственно говоря, вот несколько идей, над которыми стоит поразмыслить в поисках ответа на вопрос о том, нужна ли вам технология GraphQL.
▍Вы стремитесь уменьшить количество запросов, выполняемых с клиента
Многие приложения страдают от того, что им приходится выполнять слишком много HTTP-запросов, от того, что делать это приходится слишком часто, и от того, что это — сложные запросы. В то время как использование технологии GraphQL не позволяет полностью отказаться от выполнения запросов, эта технология, если ей правильно пользоваться, способна значительно уменьшить количество запросов, выполняемых со стороны клиента (во многих случаях для получения некоего набора связанных данных достаточно лишь одного запроса).
Является ли ваш проект приложением с множеством пользователей, или приложением, обрабатывающим огромные объёмы данных (например — это нечто вроде системы для работы с медицинскими данными), использование GraphQL определённо улучшит производительность его клиентской части.
▍Вы хотите избежать денормализации данных, проводимой лишь ради того, чтобы оптимизировать работу механизмов построения пользовательского интерфейса
В приложениях, в которых используются большие объёмы реляционных данных, часто может возникать «ловушка денормализации». Хотя такой подход и оказывается рабочим, он, вне всякого сомнения, далёк от идеала. Его применение может плохо влиять на производительность систем. Благодаря использованию GraphQL и вложенных запросов необходимость в денормализации данных значительно уменьшается.
▍У вас есть множество источников информации, к которым вы обращаетесь из разных приложений
Эта проблема может быть частично решена с помощью традиционных REST API, но даже при таком подходе одна проблема всё ещё остаётся: единообразие запросов, выполняемых с клиентской стороны. Предположим, что в ваш проект входят веб-приложение, приложения для iOS и Android, а также API для разработчиков. В подобных условиях вам, вероятнее всего, придётся, на каждой платформе, «мастерить из подручных материалов» средства для выполнения запросов.
Это ведёт к тому, что приходится поддерживать, на разных платформах, несколько реализаций HTTP, это означает отсутствие единообразия в средствах выполнения запросов и наличие запутанных конечных точек API (вы, наверняка, такое уже видели).
▍Может быть технология GraphQL — это верх совершенства? Стоит ли мне прямо сейчас выбросить мой REST API и перейти на GraphQL?
Нет, конечно. Ничто не совершенно. И, надо отметить, работать с GraphQL не так уж и просто. Для того чтобы создать работающую схему GraphQL, нужно выполнить множество обязательных шагов. Так как вы только изучаете данную технологию, это может вывести вас из равновесия, так как нелегко бывает понять то, чего именно не хватает в вашей схеме для правильной работы системы. При этом сообщения об ошибках, возникающих на клиенте и на сервере, могут оказаться не особенно полезными.
Далее, использование GraphQL на клиенте, в том, что выходит за рамки языка запросов, не стандартизовано. Хотя работу с GraphQL могут облегчить различные библиотеки, самыми популярными из которых являются Apollo и Relay, каждая из них отличается собственными специфическими особенностями.
GraphQL — это, кроме того, всего лишь спецификация. Пакеты вроде graphql
(этот пакет используется внутри пакета express-graphql
, применённого в нашем примере) — это всего лишь реализации данной спецификации. Другими словами, разные реализации GraphQL для разных языков программирования могут по-разному интерпретировать спецификацию. Это может привести к возникновению проблем, идёт ли речь о разработчике-одиночке, или о команде, в которой, при работе над разными проектами, используются разные языки программирования.
Итоги
Несмотря на то, что внедрение GraphQL может оказаться непростой задачей, эта технология представляет собой впечатляющий шаг вперёд в сфере обработки данных. GraphQL нельзя назвать лекарством от всех болезней, но с этой технологией, определённо, стоит поэкспериментировать. Начать можно, например, поразмыслив о самой запутанной и неопрятной подсистеме, используемой в вашем проекте при работе с данными, и попытавшись реализовать эту подсистему средствами GraphQL.
Кстати, тут у меня для вас приятная новость: GraphQL можно реализовывать инкрементно. Для того чтобы извлечь выгоды из применения этой технологии нет нужды переводить на GraphQL абсолютно всё. Так, постепенно вводя в проект GraphQL, можно разобраться с этой технологией самому, заинтересовать команду, и, если то, что получится, всех устроит, двигаться дальше.
Главное — помните о том, что GraphQL — это, в конечном счёте, всего лишь инструмент. Применение GraphQL не означает необходимости в полной переработке всего, что было раньше. При этом надо отметить, что GraphQL — это технология, с которой, определённо, стоит познакомиться. Многим стоит подумать и о применении этой технологии в своих проектах. В частности, если ваши проекты кажутся не особенно производительными, если вы занимаетесь разработкой сложных интерфейсов, наподобие панелей управления, лент новостей или профилей пользователей, то вы уже знаете о том, где именно вы можете опробовать GraphQL.
Уважаемые читатели! Если сегодня состоялось ваше первое знакомство с GraphQL — просим рассказать нам о том, планируете ли вы использовать эту технологию в своих проектах.