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

Выполнение запросов

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}

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

1
2
3
4
5
6
7
8
9
{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

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

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

Корневые поля и резолверы

На самом верхнем уровне любого сервера GraphQL — тип, представляющий все возможные точки входа в GraphQL API, и обычно зовется Корневой тип, или Тип Query.

В этом примере, тип Query предоставляет поле human, принимающий аргумент id. Вероятнее всего, основной функцией тут является запрос в БД и сбор объекта Human.

1
2
3
4
5
6
7
Query: {
  human(obj, args, context) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

Этот пример написан на JavaScript, конечно серверы GraphQL могут быть построены на разных языках. Обрабатывающая функция принимает три аругмента:

obj
Предыдущий объект, обычно не используется для корневого типа.
args
Аругменты, предоставленные для поля в запросе GraphQL.
context
Значение, предоставленное в каждый resolver и содержит важную контекстную информацию, как например текущий залогиненый пользователь, или доступ к БД.

Асинхронные резолверы

Давайте поближе взлянем на то, что про происходит в этой функции resolver.

1
2
3
4
5
human(obj, args, context) {
  return context.db.loadHumanByID(args.id).then(
    userData => new Human(userData)
  )
}

context используется для предоставления доступа к БД, чтобы загрузить данные для пользователя по id, представленному как аргумент в запросе GraphQL. Поскольку загрузка из БД — асинхронная операция, она возвращает Promise. В JavaScript промисы используются для работы с асинхронными значениями, но та же концепция существует во многих языках, и обычно называются Futures, Tasks или Deferred. Когда возвращается результат из БД, мы можем собрать и вернуть объект Human.

Обратите внимание, что поскольку функция resolver должна быть в курсе Promises, а запрос GraphQL — нет. Он просто ожидает, что поле human вернет что-то, в чем он сможет запросить name. Во время выполнения, GraphQL будет ожидать Promises, Futures и Tasks для завершения, чтобы продолжить работу, и будет делать это с оптимальным совмещением.

Прямые резолверы

Теперь, т. к. доступен объект Human, выполнение GraphQL может продолжаться с запрошенными полями.

1
2
3
4
5
Human: {
  name(obj, args, context) {
    return obj.name
  }
}

Сервер GraphQL использует систему типов для того, чтобы определить, что делать следущим. Даже перед тем, как поле human что-то вернет, GraphQL знает, что следующим шагом будет обработать поля в типе Human, поскольку система типов указывает на то, что поле human вернет Human.

Разрешение имени в этом случае очень прямолинейно. Вызывается функция разрешения имени, и аргумент obj — это новый объект Human, возвращенный из предыдущего поля. В этом случае, мы ожидаем, что объект Human имеет свойство name, которое мы можем читать и возвращать напрямую.

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

Скалярное принуждение

Поскольку поле name обрабатывается, поля appearsIn и starships могут быть обработаны параллельно. Поле appearsIn может так же иметь простой обработчик, но давайте поближе взглянем:

1
2
3
4
5
Human: {
  appearsIn(obj) {
    return obj.appearsIn // returns [ 4, 5, 6 ]
  }
}

Отметим, что наша система типов утверждает, что appearsIn возвращает значения Enum по определению, однако эта функция возвращает числа! Действительно, если мы взглянем на результат, мы увидим, что возвращаются подходящие значения Enum. Что происходит?

Это и есть пример скалярного принуждения. Система типов знает, что нужно ожидать, и преобразует значения, возвращенные функцией-обработчиком во что-то, что поддержит договор с API. В этом случае, на сервере может быть определен Enum, который использует числа вроде 4, 5 и 6 внутри, но представляет их как значения вида Enum в системе типов GraphQL.

Список резолверов

Мы уже увидели немного из того, что происходит, когда поле возвращает список, на примере поле appearsIn. Оно возвращает список значений enum, и поскольку это то, что ожидает система типов, каждый элемент списка преобразован в подходящее значение enum. А что происходит, когда обрабатывается поле starships?

1
2
3
4
5
6
7
8
9
Human: {
  starships(obj, args, context) {
    return obj.starshipIDs.map(
      id => context.db.loadStarshipByID(id).then(
        shipData => new Starship(shipData)
      )
    )
  }
}

Обработчик этого поля не просто возвращает Promise, он возвращает список из Promise. Объект Human имеет список из Starship ID, которые они пилотируют, но нам нужно загрузить все эти ID для создания объектов Starship.

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

Производство результата

Т. к. обработано каждое поле, результирующее значение размещено в карте key-value c полем name (или алиас) в качестве ключа и полученным значением. Это начинается с листьевых полей (конечных) запроса, и проходит весь путь наверх до оригинального поля в корне типа Query. В итоге получается структура, отражающая оригинальный запрос, который может быть отправлен (обычно как JSON) клиенту.

Давайте сделаем последний взгляд на оригинальный запрос, чтобы увидеть, все эти функции приводят к результату:

1
2
3
4
5
6
7
8
9
{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

Комментарии