Выполнение нескольких запросов
Объединяет несколько запросов в один, совместно использует между ними состояние и выполняет их в запрошенном порядке.
Описание
Выполнение нескольких запросов объединяет запросы в один, гарантируя их выполнение в том же порядке, в каком они были запрошены. Операции могут обмениваться состоянием между собой через динамические переменные, которые вычисляются только один раз, но могут считываться многократно на протяжении всего документа.
query SomeQuery {
id @export(as: "rootID")
}
query AnotherQuery
@depends(on: "SomeQuery")
{
_echo(value: $rootID )
}Эта функциональность предоставляет ряд преимуществ:
- Улучшает производительность: вместо того чтобы выполнять запрос к серверу GraphQL, ждать его ответа, а затем использовать этот результат для выполнения другого запроса, можно объединить запросы в один и выполнить их в рамках одного обращения, избежав тем самым задержки от множественных HTTP-соединений.
- Позволяет организовывать GraphQL-запросы в виде атомарных операций (или логических единиц), зависящих друг от друга, которые могут выполняться условно — в зависимости от результата предыдущей операции.
Выполнение нескольких запросов отличается от пакетной обработки запросов (query batching), при которой сервер GraphQL также выполняет несколько запросов в рамках одного обращения, однако эти запросы просто выполняются один за другим, независимо друг от друга.
Включённые директивы
Когда функция выполнения нескольких запросов включена, в схеме GraphQL становятся доступны следующие директивы:
@depends(директива операции): позволяет операции (будь тоqueryилиmutation) указать, какие другие операции должны быть выполнены перед ней@export(директива поля): экспортирует значение некоторого поля из одного запроса в динамическую переменную, которая затем передаётся в качестве входных данных в поле или директиву другого запроса@exportFrom(директива поля): аналогична@export, но предназначена для экспорта значения области-видимости динамической переменной (переданной через@passOnwards(as: "...")или@applyField(passOnwardsAs: "..."))@deferredExport(директива поля): аналогична@export, но предназначена для использования с Multi-Field Directives
Кроме того, директивы @include и @skip также доступны как директивы операции (обычно они являются лишь директивами поля) и могут использоваться для условного выполнения операции, если та удовлетворяет некоторому условию.
@depends
Когда документ GraphQL содержит несколько операций, мы указываем серверу, какую из них выполнить, с помощью параметра URL ?operationName=...; в противном случае будет выполнена последняя операция.
Начиная с этой начальной операции, сервер соберёт все операции для выполнения, которые определяются добавлением директивы depends(on: [...]), и выполнит их в соответствующем порядке с соблюдением зависимостей.
Аргумент директивы operations принимает массив имён операций ([String]), или можно также указать одно имя операции (String).
В этом запросе мы передаём ?operationName=Four, и выполненные операции (будь то query или mutation) будут ["One", "Two", "Three", "Four"]:
mutation One {
# Do something ...
}
mutation Two {
# Do something ...
}
query Three @depends(on: ["One", "Two"]) {
# Do something ...
}
query Four @depends(on: "Three") {
# Do something ...
}@export
Директива @export экспортирует значение поля (или набора полей) в динамическую переменную, которая используется в качестве входных данных в некотором поле или запросе другого запроса.
Например, в этом запросе мы экспортируем имя авторизованного пользователя и используем это значение для поиска записей, содержащих данную строку (обратите внимание, что переменная $loggedInUserName, будучи динамической, не требует определения в операции FindPosts):
query GetLoggedInUserName {
me {
name @export(as: "loggedInUserName")
}
}
query FindPosts @depends(on: "GetLoggedInUserName") {
posts(filter: { search: $loggedInUserName }) {
id
}
}@exportFrom
Аналогична @export, но вместо экспорта значения поля экспортирует значение динамической переменной с ограниченной областью видимости, переданной через @passOnwards(as: "...") или @applyField(passOnwardsAs: "...").
Например, в этом запросе мы используем @applyField для изменения элементов в массиве и присваиваем новое значение динамической переменной с ограниченной областью видимости $replaced. Затем мы используем @exportFrom, чтобы сделать это значение глобально доступным через динамическую переменную $replacedList, и его можно будет получить из последующего запроса.
query One {
originalList: _echo(value: ["Hello everyone", "How are you?"])
@underEachArrayItem(
passValueOnwardsAs: "value"
affectDirectivesUnderPos: [1, 2]
)
@applyField(
name: "_strReplace"
arguments: {
search: " "
replaceWith: "-"
in: $value
},
passOnwardsAs: "replaced"
)
@exportFrom(
scopedDynamicVariable: $replaced,
as: "replacedList"
)
}
query Two @depends(on: "One") {
transformedList: _echo(value: $replacedList)
}Это приведёт к следующему результату:
{
"data": {
"originalList": [
"Hello everyone",
"How are you?"
],
"transformedList": [
"Hello-everyone",
"How-are-you?"
]
}
}@deferredExport
Когда функция Multi-Field Directives включена и мы экспортируем значения нескольких полей в словарь, используйте @deferredExport вместо @export, чтобы гарантировать, что все директивы каждого задействованного поля были выполнены до экспорта значения поля.
Например, в этом запросе к первому полю применена директива @strUpperCase, а ко второму — @strTitleCase. При выполнении @deferredExport экспортированное значение будет содержать эти применённые директивы:
query One {
id @strUpperCase # Will be exported as "ROOT"
again: id @strTitleCase # Will be exported as "Root"
@deferredExport(as: "props", affectAdditionalFieldsUnderPos: [1])
}
query Two @depends(on: "One") {
mirrorProps: _echo(value: $props)
}Результат:
{
"data": {
"id": "ROOT",
"again": "Root",
"mirrorProps": {
"id": "ROOT",
"again": "Root"
}
}
}@skip и @include (в операциях)
Когда функция выполнения нескольких запросов включена, директивы @include и @skip также доступны как директивы операции и могут использоваться для условного выполнения операции, если та удовлетворяет некоторому условию.
Например, в этом запросе операция CheckIfPostExists экспортирует динамическую переменную $postExists и, только если её значение равно true, будет выполнена мутация ExecuteOnlyIfPostExists:
query CheckIfPostExists($id: ID!) {
# Initialize the dynamic variable to `false`
postExists: _echo(value: false) @export(as: "postExists")
post(by: { id: $id }) {
# Found the Post => Set dynamic variable to `true`
postExists: _echo(value: true) @export(as: "postExists")
}
}
mutation ExecuteOnlyIfPostExists
@depends(on: "CheckIfPostExists")
@include(if: $postExists)
{
# Do something...
}Выходные данные динамических переменных
@export может производить 6 различных выходных значений в зависимости от сочетания:
- значения аргумента
type(либоSINGLE,LIST, либоDICTIONARY) - применяется ли директива к одному полю или к нескольким полям (через модуль Multi-Field Directives)
Таким образом, 6 возможных вариантов вывода:
- Тип
SINGLE:- Одиночное поле
- Несколько полей
- Тип
LIST:- Одиночное поле
- Несколько полей
- Тип
DICTIONARY:- Одиночное поле
- Несколько полей
Тип SINGLE / Одиночное поле
Вывод представляет собой одно значение при передаче параметра type: SINGLE (который задан как значение по умолчанию).
В этом запросе:
query {
post(by: { id: 1 }) {
title @export(as: "postTitle", type: SINGLE)
}
}...динамическая переменная $postTitle будет иметь значение:
"Hello world!"Обратите внимание: если SINGLE применяется к массиву сущностей, то экспортируется значение последней сущности.
В этом запросе:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitle", type: SINGLE)
}
}...динамическая переменная $postTitle будет иметь значение для записи с ID 5:
"Everything good?"Тип SINGLE / Несколько полей
Если @export применяется к нескольким полям (путём добавления параметра affectAdditionalFieldsUnderPos, предоставляемого модулем Multi-Field Directives), то значение, устанавливаемое в динамическую переменную, является словарём { key: field alias, value: field value } (типа JSONObject).
Этот запрос:
query {
post(by: { id: 1 }) {
title
content
@export(
as: "postData",
type: SINGLE,
affectAdditionalFieldsUnderPos: [1]
)
}
}...экспортирует динамическую переменную $postData со значением:
{
"title": "Hello world!",
"content": "Lorem ipsum."
}Тип LIST / Одиночное поле
Динамическая переменная будет содержать массив со значением поля из всех запрошенных сущностей (из охватывающего поля) при передаче параметра type: LIST.
При выполнении этого запроса (в котором запрашиваемые сущности — это записи с ID 1 и 5):
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitles", type: LIST)
}
}...динамическая переменная $postTitles будет иметь значение:
[
"Hello world!",
"Everything good?"
]Тип LIST / Несколько полей
Мы получаем массив словарей (типа JSONObject), каждый из которых содержит значения полей, к которым применяется директива.
Этот запрос:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsData",
type: LIST,
affectAdditionalFieldsUnderPos: [1]
)
}
}...экспортирует динамическую переменную $postsData со значением:
[
{
"title": "Hello world!",
"content": "Lorem ipsum."
},
{
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
]Тип DICTIONARY / Одиночное поле
Динамическая переменная будет содержать словарь (типа JSONObject), где ключом является ID запрашиваемой сущности, а значением — значения поля, при передаче параметра type: DICTIONARY.
Этот запрос:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postIDTitles", type: DICTIONARY)
}
}...экспортирует динамическую переменную $postIDTitles со значением:
{
"1": "Hello world!",
"5": "Everything good?"
}Тип DICTIONARY / Несколько полей
В этой комбинации мы экспортируем словарь словарей: { key: entity ID, value: { key: field alias, value: field value } } (используя тип JSONObject, который будет содержать записи типа JSONObject).
Этот запрос:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsIDProperties",
type: DICTIONARY,
affectAdditionalFieldsUnderPos: [1]
)
}
}...экспортирует динамическую переменную $postsIDProperties со значением:
{
"1": {
"title": "Hello world!",
"content": "Lorem ipsum."
},
"5": {
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
}Экспорт значений при итерации по массиву или объекту JSON
@export учитывает кардинальность любой охватывающей мета-директивы.
В частности, когда @export вложена под мета-директиву, которая выполняет итерацию по элементам массива или свойствам объекта JSON (т.е. @underEachArrayItem и @underEachJSONObjectProperty), экспортированное значение будет массивом.
Этот запрос:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...производит $contentAttributes со значением:
[
"List Block",
"Columns Block",
"Columns inside Columns (nested inner blocks)",
"Life is so rich",
"Life is so dynamic"
]В отличие от этого, тот же запрос, который обращается к конкретному элементу массива вместо итерации по всем (заменяя @underEachArrayItem на @underArrayItem(index: 0)), экспортирует одно значение.
Этот запрос:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underArrayItem(index: 0)
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...производит $contentAttributes со значением:
"List Block"Порядок выполнения директив
Если перед @export присутствуют другие директивы, экспортированное значение будет отражать изменения, внесённые этими предыдущими директивами.
Например, в этом запросе результат будет различаться в зависимости от того, выполняется ли @export до или после @strUpperCase:
query One {
id
# First export "root", only then will be converted to "ROOT"
@export(as: "id")
@strUpperCase
again: id
# First convert to "ROOT" and then export this value
@strUpperCase
@export(as: "again")
}
query Two @depends(on: "One") {
mirrorID: _echo(value: $id)
mirrorAgain: _echo(value: $again)
}Результат:
{
"data": {
"id": "ROOT",
"again": "ROOT",
"mirrorID": "root",
"mirrorAgain": "ROOT"
}
}Выполнение в Persisted Queries
Когда запрос GraphQL содержит несколько операций в Persisted Query, мы можем вызвать соответствующий эндпоинт, передав параметр URL ?operationName=... с именем операции для выполнения; в противном случае будет выполнена последняя операция.
Например, чтобы выполнить операцию GetPostsContainingString в Persisted Query с эндпоинтом /graphql-query/posts-with-user-name/, необходимо вызвать:
https://mysite.com/graphql-query/posts-with-user-name/?operationName=GetPostsContainingStringПримеры
Импорт содержимого из внешнего API-эндпоинта:
query FetchDataFromExternalEndpoint
{
_sendJSONObjectItemHTTPRequest(input: { url: "https://site.com/wp-json/wp/posts/1" } )
@export(as: "externalData")
@remove
}
query ManipulateDataIntoInput @depends(on: "FetchDataFromExternalEndpoint")
{
title: _objectProperty(
object: $externalData,
by: {
path: "title.rendered"
}
) @export(as: "postTitle")
excerpt: _objectProperty(
object: $externalData,
by: {
key: "excerpt"
}
) @export(as: "postExcerpt")
}
mutation CreatePost @depends(on: "ManipulateDataIntoInput")
{
createPost(input: {
title: $postTitle
excerpt: $postExcerpt
}) {
id
}
}Получение данных записи, их преобразование и повторное сохранение:
query GetPostData(
$postId: ID!
) {
post(by: {id: $postId}) {
id
title @export(as: "postTitle")
rawContent @export(as: "postContent")
}
}
query AdaptPostData(
$replaceFrom: String!,
$replaceTo: String!
)
@depends(on: "GetPostData")
{
adaptedPostTitle: _strReplace(
search: $replaceFrom
replaceWith: $replaceTo
in: $postTitle
)
@export(as: "adaptedPostTitle")
adaptedPostContent: _strReplace(
search: $replaceFrom
replaceWith: $replaceTo
in: $postContent
)
@export(as: "adaptedPostContent")
}
mutation StoreAdaptedPostData(
$postId: ID!
)
@depends(on: "AdaptPostData")
{
updatePost(input: {
id: $postId,
title: $adaptedPostTitle,
contentAs: { html: $adaptedPostContent },
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
title
rawContent
}
}
}Обновление записи, если она существует, или отображение сообщения об ошибке в противном случае:
query GetPost($id: ID!) {
post(by:{id: $id}) {
id
title
}
_notNull(value: $__post) @export(as: "postExists")
}
query FailIfPostNotExists($id: ID!)
@skip(if: $postExists)
@depends(on: "GetPost")
{
errorMessage: _sprintf(
string: "There is no post with ID '%s'",
values: [$id]
) @remove
_fail(
message: $__errorMessage
data: {
id: $id
}
) @remove
}
mutation UpdatePost($id: ID!, $postTitle: String)
@include(if: $postExists)
@depends(on: "GetPost")
{
updatePost(input: {
id: $id,
title: $postTitle,
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
title
rawContent
}
}
}
query MaybeUpdatePost
@depends(on: [
"FailIfPostNotExists",
"UpdatePost"
])
{
id @remove
}Вход пользователя в систему перед выполнением мутации и немедленный выход после её завершения:
mutation LogUserIn(
$username: String!
$password: String!
) {
loginUser(by: {
credentials: {
usernameOrEmail: $username,
password: $password
}
}) @remove {
status
user {
id
username
}
}
}
mutation AddComment(
$customPostId: ID!
$commentContent: HTML!
)
@depends(on: "LogUserIn")
{
addCommentToCustomPost(input: {
customPostID: $customPostId,
commentAs: { html: $commentContent }
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
comment {
id
parent {
id
}
content
date
author {
name
email
}
}
}
}
mutation LogUserOut
@depends(on: "AddComment")
{
logoutUser @remove {
status
userID
}
}
query ExecuteAllAddCommentOperations
@depends(on: "LogUserOut")
{
id @remove
}Условный вход пользователя в систему перед выполнением мутации, если данные предоставлены:
query ExportUserLogin(
$username: String
) {
_notNull(value: $username)
@export(as: "hasUsername")
@remove
}
mutation MaybeLogUserIn(
$username: String
$password: String
)
@depends(on: "ExportUserLogin")
@include(if: $hasUsername)
{
loginUser(by: {
credentials: {
usernameOrEmail: $username,
password: $password
}
}) @remove {
status
user {
id
username
}
}
}
mutation AddComment(
$customPostId: ID!
$commentContent: HTML!
)
@depends(on: "MaybeLogUserIn")
{
addCommentToCustomPost(input: {
customPostID: $customPostId,
commentAs: { html: $commentContent }
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
comment {
id
parent {
id
}
content
date
author {
name
email
}
}
}
}
mutation MaybeLogUserOut
@depends(on: "AddComment")
@include(if: $hasUsername)
{
logoutUser @remove {
status
userID
}
}
query ExecuteAllAddCommentOperations
@depends(on: "MaybeLogUserOut")
{
id @remove
}Спецификация GraphQL
В настоящее время данная функциональность не является частью спецификации GraphQL, однако она была запрошена: