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
}
Списки и ненулевые значения
В GraphQL
можно определять объекты, скалярные типы и перечисления. Однако, при использовании этих типов в других частях схемы или при определении переменных запроса можно применять дополнительные модификаторы, выполняющие валидацию этих значений:
type Character {
name: String!
appearsIn: [Episode]!
}
Символ !
после названия типа означает, что данное поле является обязательным. При отсутствии значения обязательного поля выбрасывается исключение:
query DroidById($id: ID!) {
droid(id: $id) {
name
}
}
// переменная
{
"id": null
}
// вывод
{
"errors": [
{
"message": "Variable \"$id\" of non-null type \"ID!\" must not be null.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}
Списки работают похожим образом. Мы используем модификатор []
для пометки типа в качестве List
. Это означает, что поле должно возвращать массив определенных типов. Модификаторы ненулевого значения и списка можно комбинировать:
myField: [String!]
Это означает, что список сам по себе может быть пустым, но не может содержать нулевые значения:
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error
Определим ненулевой список строк:
myField: [String]!
Это означает, что список не может быть пустым, но может содержать нулевые значения:
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid
Интерфейсы
Интерфейс (interface) - это абстрактный тип, включающий набор полей, которые должен содержать тип для реализации данного интерфейса.
Вот пример интерфейса Character
, представляющий любого персонажа из трилогии "Звездные войны":
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
Любой тип, реализующий данный интерфейс должен содержать указанные поля с указанными аргументами и возвращаемыми типами. Вот несколько типов, реализующих Character
:
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
Типы, реализующие интерфейсы, могут содержать дополнительные поля.
Интерфейсы могут быть полезны, когда мы хотим вернуть объект или несколько объектов, но они имеют разные типы:
query HeroForEpisode($episode: Episode!) {
hero(episode: $episode) {
name
primaryFunction
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
Ошибка возникает из-за того, что интерфейс не имеет поля primaryFunction
. Для запроса поля определенного объекта следует использовать встроенный фрагмент:
query HeroForEpisode($episode: Episode!) {
hero(episode: $episode) {
name
... on Droid {
primaryFunction
}
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
Альтернативные типы
Альтернативные типы (union types) похожи на интерфейсы, но они н е определяют общие поля типов:
union SearchResult = Human | Droid | Starship
При запросе типа SearchResult
, мы можем получить Human
, Droid
или Starship
. Обратите внимание: мы не можем создавать альтернативные interface
или альтернативные union
.
При запросе, который разрешается типом SearchResult
, необходимо использовать встроенные фрагменты для запроса полей:
{
search(text: 'an') {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
// вывод
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
Поле __typename
возвращает String
, позволяющую разделять типы данных на клиенте.