Multiple Query Execution
Multiple Query ExecutionВыполнение нескольких запросов

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

Included in the “Power Extensions” bundle

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

Описание

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

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 возможных вариантов вывода:

  1. Тип SINGLE:
    1. Одиночное поле
    2. Несколько полей
  2. Тип LIST:
    1. Одиночное поле
    2. Несколько полей
  3. Тип DICTIONARY:
    1. Одиночное поле
    2. Несколько полей

Тип 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, однако она была запрошена: