Перейти к содержанию

Схемы и типы

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

Система типов

Если вы видели запрос GraphQL ранее, вы знаете, что язык запросов GraphQL - это выбор полей в объектах. Например, в данном запросе:

1
2
3
4
5
6
{
  hero {
    name
    appearsIn
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}
  • Мы начинаем со специального "root" объекта
  • Мы выбираем поле hero в нем
  • Для объекта, возвращенного в hero, мы выбираем поля name и appearsIn

Т. к. форма запроса GraphQL близко совпадает с результатом, вы можете предсказать, что вернет запрос, без детальных знаний о сервере. Однако, полезно иметь точное описание данных, которые мы можем запросить - какие поля мы можем выбрать? Какие виды объектов они могут вернуть? Какие поля доступны для этих дочерних объектов? Здесь выручает схема.

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

Язык типов

Сервисы GraphQL могут быть написаны на любом языке. Поскольку мы не можем опираться на синтаксис какого-то конкретного языка, как JavaScript например, при разговоре о схемах GraphQL, мы определим наш собственный простой язык. Мы будем использовать "GraphQL schema language" - он похож на язык запроса, и позвояет нам говорить о схемах GraphQL отрешенно от языка.

Типы объектов и поля

Самые простые компоненты схемы GraphQL - типы объектов, которые просто представляют вид объекта, который вы можете получить с вашего сервиса, и какие поля он имеет. В GraphQL schema language, мы можем отобразить это следующим образом:

1
2
3
4
type Character {
  name: String!
  appearsIn: [Episode]!
}

Язык довольно просто читаем, но давайте пройдемся по нему и расширим наш запас слов:

  • Character - тип объекта GraphQL, подразумевает его тип и некоторые поля. Большинство типов в вашей схеме будут типами объектов.
  • name и appearsIn - поля в Character. Это значит, что name и appearsIn - единственные поля, которые могут появиться в любой части запроса GraphQL, который оперирует с Character.
  • String - один из встроенных скалярных типов - это типы, которые относятся к одному скалярному объекту и не могут иметь подвыборки в запросе. Мы пройдемся по ним немного позже.
  • String! значит, что поле не может быть незаполненным (null),- сервис GraphQL обещает всегда отдавать вам значение, когда вы запрашиваете это поле. Это отмечено восклицательным знаком.
  • [Episode]! представляет массив объектов Episode. Поскольку он так же не может быть null, вы всегда можете ожидать массив (пустой или с элементами), когда вы запрашиваете поле appearsIn.

Теперь вы знаете, как выглядит тип объекта в GraphQL, и как читать основы GraphQL type language.

Аргументы

Каждое поле в типе объекта GraphQL может иметь 0 или более аргументов, например поле length ниже:

1
2
3
4
5
type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

Все аргументы именованы. В отличие от языков типа JavaScript и Python, где функции принимают список упорядоченных аргументов, все аргументы в GraphQL отправляются исключительно по имени. В этом случае, поле length имеет один определенный аргумент, unit.

Аргументы могут быть либо обязательными, либо опциональными. Если аргумент опционален, мы можем определить значение по умолчанию - если не будет передан аргумент unit, по умолчанию будет использоваться его значение METER.

Типы запрос и мутация

Большинство типов в вашей схеме будет просто нормальными типами объектов, но есть два особых типа:

1
2
3
4
schema {
  query: Query
  mutation: Mutation
}

Любой сервис GraphQL имеет тип query и может иметь или не иметь тип mutation. Эти типы подобны регулярным типам объекта, но являются особенными, т. к. определяют точку входа каждого запроса GraphаQL. Так что если вы видете запрос наподобие этого:

1
2
3
4
5
6
7
8
query {
  hero {
    name
  }
  droid(id: "2000") {
    name
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{

  "data": {
    "hero": {
      "name": "R2-D2"
    },
    "droid": {
      "name": "C-3PO"
    }
  }
}

Это значит, что сервис GraphQL должен иметь тип Query с полями hero и droid:

1
2
3
4
type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

Мутации (Mutations) работают аналогично - вы определяете поля в типе Mutation, и они будут доступны как корневые поля мутации, которые вы можете вызвать в своем запросе.

Важно помнить, что кроме того, что быть точкой входа в схему - особый статус, типы Query и Mutation являются такими же типами объектов в GraphQL, и их поля работают тем же образом.

Скалярные типы

Тип объекта GraphQL имеет имя и поля, но в некой точке эти поля должны отвечать определенной информации. Здесь вступают скалярные типы: они представляют листья запроса.

В следующем запросе, name и appearsIn отдадут скалярные типы:

1
2
3
4
5
6
{
  hero {
    name
    appearsIn
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}

Мы знаем это, т. к. эти поля не имеют подчиненных полей - это листья запроса.

GraphQL из коробки содержит набор стандартных скалярных типов:

  • Int: Подписанное 32‐bit целое число.
  • Float: Подписанное число с плавающей точкой двойной точности.
  • String: Строка в UTF‐8.
  • Boolean: true или false.
  • ID: Скалярный тип ID представляет уникальный идентификатор, обычно используемый для переполучения объекта или как ключ кеша. Тип ID сериализован так же, как String; однако, определение его как ID подразумевает, что он не должен быть распознан людьми.

В большинстве внедрений сервиса GraphQL, есть так же возможность определить собственный скалярный тип. Например, мы можем определить тип Date:

scalar Date

В этом случае уже от нашей имплементации зависит, как этот тип должен быть сериализован, десериализован и проверен. Например, вы можете определить, что тип Date всегда должен быть сериализован в формат timestamp, и ваш клиент должен ожидать, что этот формат будет у всех полей с датой.

Перечисляемые типы

Так же зовутся Enums, типы перечисления, особый вид скалярных типов, который может содержать только значение из определенного набора значений. Это позволяет вам:

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

Вот как может выглядеть определение enum в GraphQL schema language:

1
2
3
4
5
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Это значит, что где бы мы не использовали тип Episode в нашей схеме, мы будем ожидать значение NEWHOPE, EMPIRE, или JEDI.

Отметим, что имплементации сервиса GraphQL в различные языки будет иметь их собственные, зависящие от языка, пути использования enums. В языках, которые поддерживают enums как объект первого класса, имплементация может получить его преимущества; в языке вроде JavaScript без поддержки enum, эти значения могут быть связаны с набором чисел (массив). Однако, эти делатили не видны клиенту, который может работать полностью в терминах строковых имен значений перечисления.

Списки и обязательные значения

Типы объектов, скаляры и enums (перечисления) - не единственные виды типов, которые вы можете определить в GraphQL. Но когда вы используете типы в других частях схемы, или в определенях переменных вашего запроса, вы можете применить дополнительные модификаторы типов, которые влияют на проверку этих значений. Взглянем на пример:

1
2
3
4
type Character {
  name: String!
  appearsIn: [Episode]!
}

Здесь, мы используем тип String и делаем его Non-Null, добавляя восклицательный знак после имени типа. Это значит, что наш сервер всегда ожидает вернуть непустое значение этого поля, а если вернет, это вызовет ошибку исключения в GraphQL.

Модификатор типа Non-Null может быть так же использован, когда определяются аргументы поля, что побуждает сервер GraphQL возвращать ошибку проверки на null переданного аргумента.

1
2
3
4
5
query DroidById($id: ID!) {
  droid(id: $id) {
    name
  }
}
1
2
3
{
  "id": null
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "errors": [
    {
      "message": "Variable \"$id\" of required type \"ID!\" was not provided.",
      "locations": [
        {
          "line": 1,
          "column": 17
        }
      ]
    }
  ]
}

Списки работают по тому же принципу: мы можем использовать модификатор типа, чтобы отметить тип как List, что означает, что это поле вернет массив с этим типом. В языке схемы, это отображено как оборачивание типа в квадратные скобки, [ и ]. Так же и с аргументами, где этап проверки будет ожидать массив этих значений.

Модификаторы Non-Null and List могут комбинироваться. Например, вы можете иметь List с со значениями Non-Null Strings:

1
myField: [String!]

Это значит, что список сам по себе может быть null, но не может иметь null члены. Например, в JSON:

1
2
3
myField: null // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error

Теперь, скажем, мы определим Non-Null List of Strings:

1
myField: [String]!

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

1
2
3
myField: null // error
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid

Вы можете произвольно разместить любое количество модификаторов Non-Null и Non-Null, в соответствии с вашими нуждами.

Интерфейсы

Как и многие системы, GraphQL поддерживает интерфейсы. Интерфейс - это тип абстракции, который включает определенный набор полей, которые тип должен включить для внедрения интерфейса.

Например, у вас есть интерфейс Character, который представяет любого персонажа в трилогии Star Wars:

1
2
3
4
5
6
interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

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

Например, вот несколько типов, которые могут внедрить Character:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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
}

Вы можете видеть, что оба этих типа имеют все поля из интерфейса Character, но так же и дополнительные поля totalCredits, starships и primaryFunction, особых для данного конкретного типа.

Интерфейсы полезны, когда вы хотите вернуть объект или набор объектов, но они могут быть различных типов. Например, следующий запрос генерирует ошибку:

1
2
3
4
5
6
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    primaryFunction
  }
}
1
2
3
{
  "ep": "JEDI"
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "errors": [
    {
      "message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline     fragment on \"Droid\"?",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ]
    }
  ]
}

Поле hero возвращает тип Character, что означает, что это может быть Human или Droid, в зависимости от аргумента episode. В запросе выше, вы можете запросить только поля, которые существуют в интерфейсе Character, который не включает primaryFunction.

Чтобы запросить поле из определенного типа объекта, вам нужно использовать такой фрагмент:

1
2
3
4
5
6
7
8
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}
1
2
3
{
  "ep": "JEDI"
}
1
2
3
4
5
6
7
8
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

Более подробно об этом можно прочитать в разделе inline fragments инструкции к запросам.

Объединенные типы

Типы Union очень похожи на интерфейсы, но не определяют общие поля между типами.

union SearchResult = Human | Droid | Starship

Когда возвращается тип SearchResult в нашей схеме, мы можем получить Human, Droid или Starship. Отметим, что члены типа union должны быть конкретными типами объекта; вы не можете создать тип union из интерфейса или других union.

В этом случае, если вы запрашиваете поле, которое возвращает тип union SearchResult, вам нужно использовать фрагмент с условиями, который позволит запросить любое поле:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  search(text: "an") {
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "data": {
    "search": [
      {
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}

Типы ввода

Только недавно мы говорили о передаче скалярных значений, таких как enums или strings, как аргументы в поле. Но вы так же можете с легкостью передать целые объекты. Это полезно в частности в случае мутаций, где вы можете захотеть передать данные в целом объекте. В GraphQL schema language, типы ввода выглядят точно так же, как регулярные типы объектов, но с ключевым словом input вместо type:

1
2
3
4
input ReviewInput {
  stars: Int!
  commentary: String
}

Вот как вы можете использовать объект ввода в мутации:

1
2
3
4
5
6
7
8
9
mutation CreateReviewForEpisode(
  $ep: Episode!
  $review: ReviewInput!
) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
1
2
3
4
5
6
7
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
1
2
3
4
5
6
7
8
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

Поля в объекте типа объекта ввода могут самостоятельно соотноситься с типами объектов, но вы не можете смешивать типы ввода и вывода в вашей схеме. Типы объектов ввода так же не могут иметь аргументов к полям.

Комментарии