GraphQL
GraphQL - это язык запросов для
API
и с ерверного окружения, предназначенный для выполнения запросов с помощью системы типов, определенных для данных. СервисGraphQL
создается посредством определения типов и их полей и создания функций для каждого поля каждого типа.
Запросы и мутации
Поля
В простейшем случае, GraphQL позволяет запрашивать (получать) поля объекта:
{
hero {
name
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
С тем же успехом можно получать поля вложенных объектов:
{
hero {
name
# Запросы могут иметь комментарии
friends {
name
}
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Аргументы
GraphQL позволяет передавать полям объектов аргументы в момент их запроса:
{
human(id: '123') {
name
height
}
}
// вывод
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
Это, в частности, позволяет передавать аргументы нескольким полям одновременно, что может существенно сократить количество запросов к серверу:
{
human(id: '123') {
name
height(unit: FOOT)
}
}
// вывод
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
Здесь мы имеем дело с перечислением (enumeration type): значение длины может быть выражено либо в м етрах (METER), либо в футах (FOOT). Перечисления могут расширяться сервером.
Синонимы
Синонимы (aliases) позволяют использовать одинаковые названия полей с разными аргументами (переименовывать возвращаемый результат):
{
# Синоним: название поля
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
// вывод
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
Фрагменты
Фрагменты позволяют определять набор полей для повторного использования в запросах:
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
// вывод
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Использование переменных внутри фрагментов
Фрагменты имеют доступ к переменным, определенным в запросе или мутации:
query HeroComparison($first: Int = 3) {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
friendsConnection(first: $first) {
totalCount
edges {
node {
name
}
}
}
}
// вывод
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"friendsConnection": {
"totalCount": 4,
"edges": [
{
"node": {
"name": "Han Solo"
}
},
{
"node": {
"name": "Leia Organa"
}
},
{
"node": {
"name": "C-3PO"
}
}
]
}
},
"rightComparison": {
"name": "R2-D2",
"friendsConnection": {
"totalCount": 3,
"edges": [
{
"node": {
"name": "Luke Skywalker"
}
},
{
"node": {
"name": "Han Solo"
}
},
{
"node": {
"name": "Leia Organa"
}
}
]
}
}
}
}
Название операции
Для обозначения типа операции используется ключевое слово query
, за которым следует название операции:
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
Типом операции может быть запрос (query), мутация (mutation) или подписка (subscribe).
Переменные
Для того, чтобы иметь возможность работать с переменными, необходимо сделать следующее:
- Заменить статическое значение в запросе на
$variableName
- Определить
$variableName
в качестве параметра запроса - Передать
variableName: value
в транспортируемом формате (обычно, таким форматом являетсяJSON
)
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
$episode: Episode
означает, что переменная episode
должна соответствовать типу Episode
. !
после типа означает, что переменная является обязательной (в приведенном примере переменная является опциональной).
Дефолтные переменные
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
Директивы
Директивы (directives) позволяют конструировать запросы динамически:
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends)
}
}
// переменные
{
"episode": "JEDI",
"withFriends": false
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
На сегодняшний день GraphQL предоставляет 2 директивы:
@include(if: Boolean)
- включение полей при удовлетворении условия@skip(if: Boolean)
- пропуск полей
Директивы могут расширяться сервером.
Мутации
Если запросы только возвращают данные, то мутации позволяют эти данные изменять. В ответ на мутацию возвращается новое состояние объекта:
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
stars
comment
}
}
// переменные
{
"episode": "JEDI",
"review": {
"stars": 5,
"comment": "Это великий фильм!"
}
}
// вывод
{
"data": {
"createReview": {
"stars": 5,
"comment": "Это великий фильм!"
}
}
}
Обратите внимание, что типом $review
является специальный тип объекта - входной (или входящий) объект (или данные) (input object type).
Как и запросы, мутации могут содержать несколько полей. Основное отличие между ними состоит в том, что запросы выполняются параллельно, а мутации - последовательно. Это объясняется тем, что первая мутация должна завершиться до начала следующей, так как обе мутации могут изменять одни и те же данные.
Встроенные фрагменты
Схемы GraphQL
позволяют определять интерфейсы (interfaces) и альтернативные типы (union types). В случае, когда в ответ на запрос возвращается интерфейс или альтернативный тип, для доступа к данным нижележащего конкретного типа (concrete type) используются встроенные фрагменты:
query HeroForEpisode($episode: Episode!) {
hero(episode: $episode) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
В данном запросе поле hero
возвращает тип Character
, который может быть Human
или Droid
в зависимости от аргумента episode
. Мы можем запрашивать только те поля, которые существуют в интерфейсе Character
, такие как name
.
Для запроса поля конкретного типа используются встроенные фрагменты c условием. Поскольку первый аргумент помечен как ... on Droid
, поле primaryFunction
будет выполняться, только если Character
, возвращенный из hero
, будет иметь тип Droid
. То же самое справедливо для поля height
типа Human
.
Поля с метаданными
Мета-поле __typename
позволяет получить название типа объекта в любом месте запроса:
{
search(text: 'an') {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
// вывод
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
Схемы и типы
Система типов
В следующем примере мы начинаем со специального объекта root
, выбираем его поле hero
, которое является объектом, затем выбираем поля name
и appearsIn
этого объекта:
{
hero {
name
appearsIn
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Каждый GraphQL-сервис определяет набор типов, описывающих данные, которые можно запрашивать. При получении запросов, они проверяются и выполняются с помощью схемы. При этом, сервис может быть реализован на любом языке, а не только на JavaScript
.
Типы и поля объектов
Основными компонентами схем являются объекты (object types):
type Character {
name: String!
appearsIn: [Episode!]!
}
Character
- это объектный тип, содержащий поля. Большая часть типов любой схемы является объектамиname
иappearsIn
- поля типаCharacter
. Только эти поля можно запрашиватьString
- один из встроенных скалярных типов (scalar types). Скалярные типы не могут содержать полейString!
- означает, что поле является ненулевым, не может иметь нулевое значение, т.е. является обязательным[Episode!]!
- обязательный (ненулевой) массив, содержащий объекты с обязательным типомEpisode
Аргументы
Каждое поле может иметь аргументы:
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
Все аргументы являются именованными, т.е. аргументы передаются по названию. Аргументы могут быть обязательными или опциональными. В последнем случае можно определять дефолтные значения.
Запрос и мутация
В схеме существует два специальных типа:
schema {
query: Query
mutation: Mutation
}
Каждый сервис имеет тип query
и может иметь тип mutation
. Эти типы являются входными точками для запросов. Такой запрос:
query {
hero {
name
}
droid(id: '123') {
name
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2"
},
"droid": {
"name": "C-3PO"
}
}
}
Означает, что сервис должен иметь тип Query
с полями hero
и droid
:
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
Мутации работают похожим образом.
Скалярные типы
Объект имеет название и поля, но в определенный момент эти поля должны разрешиться в конкретные данные - скалярные типы:
{
hero {
name
appearsIn
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Дефолтные скалярные типы:
Int
- 32-битное целое число со знакомFloat
- число двойной точности со знакомString
- строка, последовательность символовUTF-8
Boolean
-true
илиfalse
ID
- уникальный идентификатор, используемый для получения объектов, а также в качестве кл ючей для кэша
Допускается определять кастомные скалярные типы:
scalar Date
Перечисления
Enums
- специальный скалярный тип, набор допустимых значений:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}