Multiple Query Execution
Multiple Query ExecutionExecução de Múltiplas Queries

Execução de Múltiplas Queries

Included in the “Power Extensions” bundle

Combine múltiplas queries em uma única query, compartilhando estado entre elas e executando-as na ordem solicitada.

Descrição

A execução de múltiplas queries combina várias queries em uma única query, garantindo que sejam executadas na mesma ordem solicitada. As operações podem comunicar estado entre si por meio de variáveis dinâmicas, que são calculadas apenas uma vez, mas podem ser lidas várias vezes ao longo do documento.

query SomeQuery {
  id @export(as: "rootID")
}
 
query AnotherQuery
  @depends(on: "SomeQuery")
{
  _echo(value: $rootID )
}

Esse recurso oferece vários benefícios:

  • Melhora o desempenho: em vez de executar uma query contra o servidor GraphQL, aguardar sua resposta e então usar esse resultado para executar outra query, podemos combinar as queries em uma só e executá-las em uma única requisição, evitando assim a latência de múltiplas conexões HTTP.
  • Nos permite gerenciar nossas queries GraphQL como operações atômicas (ou unidades lógicas) que dependem umas das outras e que podem ser executadas condicionalmente com base no resultado de uma operação anterior.

A execução de múltiplas queries é diferente do query batching, no qual o servidor GraphQL também executa múltiplas queries em uma única requisição, mas essas queries são apenas executadas uma após a outra, de forma independente entre si.

Directives habilitadas

Quando a execução de múltiplas queries está habilitada, as seguintes directives são disponibilizadas no schema GraphQL:

  • @depends (directive de operação): para que uma operação (seja uma query ou mutation) indique quais outras operações devem ser executadas antes
  • @export (directive de campo): para exportar o valor de um campo de uma query como uma variável dinâmica, a ser usada como input em algum campo ou directive em outra query
  • @exportFrom (directive de campo): semelhante a @export, mas para exportar o valor de uma variável dinâmica com escopo (passada via @passOnwards(as: "...") ou @applyField(passOnwardsAs: "..."))
  • @deferredExport (directive de campo): semelhante a @export, mas para ser usada com Multi-Field Directives

Além disso, as directives @include e @skip também são disponibilizadas como directives de operação (normalmente são apenas directives de campo), e podem ser usadas para executar condicionalmente uma operação se ela satisfizer alguma condição.

@depends

Quando o documento GraphQL contém múltiplas operações, indicamos ao servidor qual delas executar via parâmetro de URL ?operationName=...; caso contrário, a última operação será executada.

A partir dessa operação inicial, o servidor coletará todas as operações a executar, que são definidas adicionando a directive depends(on: [...]), e as executará na ordem correspondente, respeitando as dependências.

O argumento operations da directive recebe um array de nomes de operação ([String]), ou podemos também fornecer um único nome de operação (String).

Nesta query, passamos ?operationName=Four, e as operações executadas (sejam query ou mutation) serão ["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

A directive @export exporta o valor de um campo (ou conjunto de campos) para uma variável dinâmica, a ser usada como input em algum campo ou query de outra query.

Por exemplo, nesta query exportamos o nome do usuário logado e usamos esse valor para buscar posts que contêm essa string (observe que a variável $loggedInUserName, por ser dinâmica, não precisa ser definida na operação FindPosts):

query GetLoggedInUserName {
  me {
    name @export(as: "loggedInUserName")
  }
}
 
query FindPosts @depends(on: "GetLoggedInUserName") {
  posts(filter: { search: $loggedInUserName }) {
    id
  }
}

@exportFrom

É semelhante a @export, mas em vez de exportar o valor do campo, exporta o valor de uma variável dinâmica com escopo, passada via @passOnwards(as: "...") ou @applyField(passOnwardsAs: "...").

Por exemplo, nesta query usamos @applyField para modificar os elementos do array e atribuir esse novo valor à variável dinâmica com escopo $replaced. Em seguida, usamos @exportFrom para tornar esse valor acessível globalmente via variável dinâmica $replacedList, de modo que possa ser recuperado por uma query subsequente.

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)
}

Isso produzirá:

{
  "data": {
    "originalList": [
      "Hello everyone",
      "How are you?"
    ],
    "transformedList": [
      "Hello-everyone",
      "How-are-you?"
    ]
  }
}

@deferredExport

Quando o recurso Multi-Field Directives está habilitado e exportamos o valor de múltiplos campos para um dicionário, use @deferredExport em vez de @export para garantir que todas as directives de cada campo envolvido tenham sido executadas antes de exportar o valor do campo.

Por exemplo, nesta query, o primeiro campo tem a directive @strUpperCase aplicada a ele, e o segundo tem @strTitleCase. Ao executar @deferredExport, o valor exportado terá essas directives aplicadas:

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)
}

Produzindo:

{
  "data": {
    "id": "ROOT",
    "again": "Root",
    "mirrorProps": {
      "id": "ROOT",
      "again": "Root"
    }
  }
}

@skip e @include (em operações)

Quando a execução de múltiplas queries está habilitada, as directives @include e @skip também estão disponíveis como directives de operação, e podem ser usadas para executar condicionalmente uma operação se ela satisfizer alguma condição.

Por exemplo, nesta query, a operação CheckIfPostExists exporta uma variável dinâmica $postExists e, somente se seu valor for true, a mutation ExecuteOnlyIfPostExists será executada:

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...
}

Saídas de variáveis dinâmicas

@export pode produzir 6 saídas diferentes, com base em uma combinação de:

  • O valor do argumento type (SINGLE, LIST ou DICTIONARY)
  • Se a directive é aplicada a um único campo ou a múltiplos campos (via módulo Multi-Field Directives)

As 6 saídas possíveis são:

  1. Tipo SINGLE:
    1. Campo único
    2. Multi-campo
  2. Tipo LIST:
    1. Campo único
    2. Multi-campo
  3. Tipo DICTIONARY:
    1. Campo único
    2. Multi-campo

Tipo SINGLE / Campo único

A saída é um valor único ao passar o parâmetro type: SINGLE (definido como valor padrão).

Nesta query:

query {
  post(by: { id: 1 }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...a variável dinâmica $postTitle terá o valor:

"Hello world!"

Observe que se SINGLE for aplicado sobre um array de entidades, o valor da última entidade é o que será exportado.

Nesta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...a variável dinâmica $postTitle terá o valor do post com ID 5:

"Everything good?"

Tipo SINGLE / Multi-campo

Se @export for aplicado em vários campos (adicionando o parâmetro affectAdditionalFieldsUnderPos fornecido pelo módulo Multi-Field Directives), o valor definido na variável dinâmica é um dicionário de { key: field alias, value: field value } (do tipo JSONObject).

Esta query:

query {
  post(by: { id: 1 }) {
    title
    content
      @export(
        as: "postData",
        type: SINGLE,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exporta a variável dinâmica $postData com o valor:

{
  "title": "Hello world!",
  "content": "Lorem ipsum."
}

Tipo LIST / Campo único

A variável dinâmica conterá um array com o valor do campo de todas as entidades consultadas (do campo que as engloba), passando o parâmetro type: LIST.

Ao executar esta query (na qual as entidades consultadas são posts com ID 1 e 5):

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitles", type: LIST)
  }
}

...a variável dinâmica $postTitles terá o valor:

[
  "Hello world!",
  "Everything good?"
]

Tipo LIST / Multi-campo

Obtemos um array de dicionários (do tipo JSONObject), cada um contendo os valores dos campos nos quais a directive é aplicada.

Esta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsData",
        type: LIST,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exporta a variável dinâmica $postsData com o valor:

[
  {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
]

Tipo DICTIONARY / Campo único

A variável dinâmica conterá um dicionário (do tipo JSONObject) com o ID da entidade consultada como chave e os valores do campo como valor, passando o parâmetro type: DICTIONARY.

Esta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postIDTitles", type: DICTIONARY)
  }
}

...exporta a variável dinâmica $postIDTitles com o valor:

{
  "1": "Hello world!",
  "5": "Everything good?"
}

Tipo DICTIONARY / Multi-campo

Nesta combinação, exportamos um dicionário de dicionários: { key: entity ID, value: { key: field alias, value: field value } } (usando um tipo JSONObject que conterá entradas do tipo JSONObject).

Esta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsIDProperties",
        type: DICTIONARY,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exporta a variável dinâmica $postsIDProperties com o valor:

{
  "1": {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  "5": {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
}

Exportando valores ao iterar um array ou objeto JSON

@export respeita a cardinalidade de qualquer meta-directive que a englobe.

Em particular, sempre que @export estiver aninhada abaixo de uma meta-directive que itera sobre itens de array ou propriedades de objeto JSON (ou seja, @underEachArrayItem e @underEachJSONObjectProperty), o valor exportado será um array.

Esta query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underEachArrayItem
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produz $contentAttributes com o valor:

[
  "List Block",
  "Columns Block",
  "Columns inside Columns (nested inner blocks)",
  "Life is so rich",
  "Life is so dynamic"
]

Em contraste, a mesma query que acessa um item específico no array em vez de iterar sobre todos eles (substituindo @underEachArrayItem por @underArrayItem(index: 0)) exportará um valor único.

Esta query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underArrayItem(index: 0)
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produz $contentAttributes com o valor:

"List Block"

Ordem de execução das directives

Se houver outras directives antes de @export, o valor exportado refletirá as modificações feitas por essas directives anteriores.

Por exemplo, nesta query, dependendo de @export ocorrer antes ou depois de @strUpperCase, o resultado será diferente:

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)
}

Produzindo:

{
  "data": {
    "id": "ROOT",
    "again": "ROOT",
    "mirrorID": "root",
    "mirrorAgain": "ROOT"
  }
}

Execução em Persisted Queries

Quando uma query GraphQL contém múltiplas operações em uma Persisted Query, podemos invocar o endpoint correspondente passando o parâmetro de URL ?operationName=... com o nome da operação a executar; caso contrário, a última operação será executada.

Por exemplo, para executar a operação GetPostsContainingString em uma Persisted Query com endpoint /graphql-query/posts-with-user-name/, devemos invocar:

https://mysite.com/graphql-query/posts-with-user-name/?operationName=GetPostsContainingString

Exemplos

Importar conteúdo de um endpoint de API externo:

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
  }
}

Recuperar os dados de um post, transformá-los e armazená-los novamente:

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
    }
  }
}

Atualizar um post se ele existir, ou exibir uma mensagem de erro caso contrário:

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
}

Autenticar o usuário antes de executar uma mutation, e desautenticá-lo imediatamente depois:

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
}

Autenticar condicionalmente o usuário antes de executar uma mutation, se as credenciais forem fornecidas:

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
}

Especificação GraphQL

Esta funcionalidade atualmente não faz parte da especificação GraphQL, mas foi solicitada: