Multiple Query Execution
Multiple Query ExecutionEsecuzione di Query Multiple

Esecuzione di Query Multiple

Included in the “Power Extensions” bundle

Combina più query in una sola, condividendo lo stato tra di esse ed eseguendole nell'ordine richiesto.

Descrizione

L'esecuzione di query multiple combina più query in una sola, assicurandosi che vengano eseguite nel medesimo ordine richiesto. Le operazioni possono comunicarsi lo stato a vicenda tramite variabili dinamiche, che vengono calcolate una sola volta ma possono essere lette più volte all'interno del documento.

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

Questa funzionalità offre diversi vantaggi:

  • Migliora le prestazioni: invece di eseguire una query contro il server GraphQL, attenderne la risposta e poi utilizzare quel risultato per eseguire un'altra query, possiamo combinare le query insieme in una sola ed eseguirle in un'unica richiesta, evitando così la latenza dovuta alle molteplici connessioni HTTP.
  • Ci consente di gestire le nostre query GraphQL come operazioni atomiche (o unità logiche) che dipendono le une dalle altre e che possono essere eseguite condizionalmente in base al risultato di un'operazione precedente.

L'esecuzione di query multiple è diversa dal query batching, in cui il server GraphQL esegue anch'esso più query in un'unica richiesta, ma tali query vengono semplicemente eseguite una dopo l'altra, indipendentemente l'una dall'altra.

Direttive abilitate

Quando l'esecuzione di query multiple è abilitata, le seguenti direttive vengono rese disponibili nello schema GraphQL:

  • @depends (direttiva di operazione): per fare in modo che un'operazione (che sia una query o una mutation) indichi quali altre operazioni devono essere eseguite prima
  • @export (direttiva di campo): per esportare il valore di un campo da una query come variabile dinamica, da usare come input per un campo o una direttiva in un'altra query
  • @exportFrom (direttiva di campo): simile a @export, ma per esportare il valore di una variabile dinamica con ambito limitato (passata tramite @passOnwards(as: "...") o @applyField(passOnwardsAs: "..."))
  • @deferredExport (direttiva di campo): simile a @export, ma da usare con le Multi-Field Directives

Inoltre, le direttive @include e @skip vengono rese disponibili anche come direttive di operazione (normalmente sono solo direttive di campo), e possono essere utilizzate per eseguire condizionalmente un'operazione se soddisfa una determinata condizione.

@depends

Quando il documento GraphQL contiene più operazioni, indichiamo al server quale eseguire tramite il parametro URL ?operationName=...; in caso contrario, verrà eseguita l'ultima operazione.

A partire da questa operazione iniziale, il server raccoglierà tutte le operazioni da eseguire, che vengono definite aggiungendo la direttiva depends(on: [...]), e le eseguirà nell'ordine corrispondente rispettando le dipendenze.

L'argomento operations della direttiva riceve un array di nomi di operazione ([String]), oppure possiamo anche fornire un singolo nome di operazione (String).

In questa query, passiamo ?operationName=Four, e le operazioni eseguite (che siano query o mutation) saranno ["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

La direttiva @export esporta il valore di un campo (o di un insieme di campi) in una variabile dinamica, da usare come input per un campo o una query in un'altra query.

Ad esempio, in questa query esportiamo il nome dell'utente connesso e utilizziamo questo valore per cercare articoli contenenti tale stringa (nota che la variabile $loggedInUserName, essendo dinamica, non deve essere definita nell'operazione FindPosts):

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

@exportFrom

È simile a @export, ma invece di esportare il valore del campo, esporta il valore di una variabile dinamica con ambito limitato, passata tramite @passOnwards(as: "...") o @applyField(passOnwardsAs: "...").

Ad esempio, in questa query utilizziamo @applyField per modificare gli elementi dell'array e assegnare questo nuovo valore alla variabile dinamica con ambito limitato $replaced. Successivamente, utilizziamo @exportFrom per rendere quel valore accessibile globalmente tramite la variabile dinamica $replacedList, in modo che possa essere recuperato da una query successiva.

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

Questo produrrà:

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

@deferredExport

Quando la funzionalità Multi-Field Directives è abilitata ed esportiamo il valore di più campi in un dizionario, utilizza @deferredExport al posto di @export per garantire che tutte le direttive di ciascun campo coinvolto siano state eseguite prima di esportare il valore del campo.

Ad esempio, in questa query, al primo campo è applicata la direttiva @strUpperCase, e al secondo @strTitleCase. Durante l'esecuzione di @deferredExport, il valore esportato avrà queste direttive applicate:

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

Producendo:

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

@skip e @include (nelle operazioni)

Quando l'esecuzione di query multiple è abilitata, le direttive @include e @skip sono disponibili anche come direttive di operazione, e possono essere utilizzate per eseguire condizionalmente un'operazione se soddisfa una determinata condizione.

Ad esempio, in questa query, l'operazione CheckIfPostExists esporta una variabile dinamica $postExists e, solo se il suo valore è true, la mutation ExecuteOnlyIfPostExists verrà eseguita:

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

Output delle variabili dinamiche

@export può produrre 6 output diversi, in base a una combinazione di:

  • Il valore dell'argomento type (SINGLE, LIST o DICTIONARY)
  • Se la direttiva è applicata a un singolo campo, oppure a più campi (tramite il modulo Multi-Field Directives)

I 6 possibili output sono quindi:

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

Tipo SINGLE / Campo singolo

L'output è un valore singolo quando si passa il parametro type: SINGLE (impostato come valore predefinito).

In questa query:

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

...la variabile dinamica $postTitle avrà il valore:

"Hello world!"

Nota che se SINGLE viene applicato su un array di entità, il valore esportato è quello dell'ultima entità.

In questa query:

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

...la variabile dinamica $postTitle avrà il valore dell'articolo con ID 5:

"Everything good?"

Tipo SINGLE / Multi-campo

Se @export viene applicato su più campi (aggiungendo il parametro affectAdditionalFieldsUnderPos fornito dal modulo Multi-Field Directives), allora il valore impostato sulla variabile dinamica è un dizionario di { key: field alias, value: field value } (di tipo JSONObject).

Questa query:

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

...esporta la variabile dinamica $postData con il valore:

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

Tipo LIST / Campo singolo

La variabile dinamica conterrà un array con il valore del campo di tutte le entità interrogate (dal campo che le racchiude), passando il parametro type: LIST.

Eseguendo questa query (in cui le entità interrogate sono articoli con ID 1 e 5):

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

...la variabile dinamica $postTitles avrà il valore:

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

Tipo LIST / Multi-campo

Otteniamo un array di dizionari (di tipo JSONObject), ciascuno contenente i valori dei campi su cui è applicata la direttiva.

Questa query:

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

...esporta la variabile dinamica $postsData con il valore:

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

Tipo DICTIONARY / Campo singolo

La variabile dinamica conterrà un dizionario (di tipo JSONObject) con l'ID dell'entità interrogata come chiave e i valori del campo come valore, passando il parametro type: DICTIONARY.

Questa query:

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

...esporta la variabile dinamica $postIDTitles con il valore:

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

Tipo DICTIONARY / Multi-campo

In questa combinazione, esportiamo un dizionario di dizionari: { key: entity ID, value: { key: field alias, value: field value } } (utilizzando un tipo JSONObject che conterrà voci di tipo JSONObject).

Questa query:

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

...esporta la variabile dinamica $postsIDProperties con il valore:

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

Esportazione di valori durante l'iterazione di un array o di un oggetto JSON

@export rispetta la cardinalità di qualsiasi meta-direttiva che la racchiude.

In particolare, ogni volta che @export è annidata sotto una meta-direttiva che itera su elementi di array o proprietà di oggetti JSON (ovvero @underEachArrayItem e @underEachJSONObjectProperty), il valore esportato sarà un array.

Questa query:

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

...produce $contentAttributes con il valore:

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

Al contrario, la stessa query che accede a un elemento specifico dell'array invece di iterare su tutti (sostituendo @underEachArrayItem con @underArrayItem(index: 0)) esporterà un valore singolo.

Questa query:

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

...produce $contentAttributes con il valore:

"List Block"

Ordine di esecuzione delle direttive

Se sono presenti altre direttive prima di @export, il valore esportato rifletterà le modifiche apportate da tali direttive precedenti.

Ad esempio, in questa query, a seconda che @export avvenga prima o dopo @strUpperCase, il risultato sarà diverso:

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

Producendo:

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

Esecuzione nelle Persisted Queries

Quando una query GraphQL contiene più operazioni in una Persisted Query, possiamo invocare l'endpoint corrispondente passando il parametro URL ?operationName=... con il nome dell'operazione da eseguire; in caso contrario, verrà eseguita l'ultima operazione.

Ad esempio, per eseguire l'operazione GetPostsContainingString in una Persisted Query con l'endpoint /graphql-query/posts-with-user-name/, dobbiamo invocare:

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

Esempi

Importare contenuti da un endpoint di API esterno:

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

Recuperare i dati di un articolo, trasformarli e memorizzarli di nuovo:

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

Aggiornare un articolo se esiste, altrimenti mostrare un messaggio di errore:

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
}

Connettere l'utente prima di eseguire una mutation, e disconnetterlo subito dopo:

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
}

Connettere condizionalmente l'utente prima di eseguire una mutation, se le credenziali sono fornite:

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
}

Specifica GraphQL

Questa funzionalità attualmente non fa parte della specifica GraphQL, ma è stata richiesta: