Multiple Query Execution
Multiple Query ExecutionExécution de Requêtes Multiples

Exécution de Requêtes Multiples

Included in the “Power Extensions” bundle

Combinez plusieurs requêtes en une seule, en partageant l'état entre elles et en les exécutant dans l'ordre demandé.

Description

L'exécution de requêtes multiples combine les requêtes en une seule, en s'assurant qu'elles sont exécutées dans le même ordre demandé. Les opérations peuvent communiquer leur état entre elles via des variables dynamiques, qui sont calculées une seule fois mais peuvent être lues plusieurs fois dans le document.

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

Cette fonctionnalité offre plusieurs avantages :

  • Elle améliore les performances : au lieu d'exécuter une requête contre le serveur GraphQL, d'attendre sa réponse, puis d'utiliser ce résultat pour exécuter une autre requête, nous pouvons combiner les requêtes en une seule et les exécuter dans une unique demande, évitant ainsi la latence due aux multiples connexions HTTP.
  • Elle nous permet de gérer nos requêtes GraphQL comme des opérations atomiques (ou unités logiques) qui dépendent les unes des autres, et qui peuvent être exécutées conditionnellement en fonction du résultat d'une opération précédente.

L'exécution de requêtes multiples est différente du query batching, dans lequel le serveur GraphQL exécute également plusieurs requêtes dans une seule demande, mais ces requêtes sont simplement exécutées l'une après l'autre, indépendamment les unes des autres.

Directives activées

Lorsque l'exécution de requêtes multiples est activée, les directives suivantes sont disponibles dans le schéma GraphQL :

  • @depends (directive d'opération) : pour qu'une opération (qu'il s'agisse d'une query ou d'une mutation) indique quelles autres opérations doivent être exécutées avant
  • @export (directive de champ) : pour exporter la valeur d'un champ d'une requête en tant que variable dynamique, à utiliser en entrée d'un champ ou d'une directive dans une autre requête
  • @exportFrom (directive de champ) : similaire à @export, mais pour exporter la valeur d'une variable dynamique à portée limitée (transmise via @passOnwards(as: "...") ou @applyField(passOnwardsAs: "..."))
  • @deferredExport (directive de champ) : similaire à @export, mais à utiliser avec les Multi-Field Directives

De plus, les directives @include et @skip sont également disponibles en tant que directives d'opération (elles sont normalement uniquement des directives de champ), et elles peuvent être utilisées pour exécuter conditionnellement une opération si elle satisfait une condition.

@depends

Lorsque le document GraphQL contient plusieurs opérations, nous indiquons au serveur laquelle exécuter via le paramètre URL ?operationName=... ; sinon, la dernière opération sera exécutée.

À partir de cette opération initiale, le serveur collectera toutes les opérations à exécuter, qui sont définies en ajoutant la directive depends(on: [...]), et les exécutera dans l'ordre correspondant en respectant les dépendances.

L'argument operations de la directive reçoit un tableau de noms d'opération ([String]), ou on peut également fournir un seul nom d'opération (String).

Dans cette requête, nous passons ?operationName=Four, et les opérations exécutées (qu'il s'agisse de query ou de mutation) seront ["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 directive @export exporte la valeur d'un champ (ou d'un ensemble de champs) dans une variable dynamique, à utiliser en entrée d'un champ ou d'une requête dans une autre requête.

Par exemple, dans cette requête, nous exportons le nom de l'utilisateur connecté et utilisons cette valeur pour rechercher des articles contenant cette chaîne (notez que la variable $loggedInUserName, étant dynamique, n'a pas besoin d'être définie dans l'opération FindPosts) :

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

@exportFrom

Elle est similaire à @export, mais au lieu d'exporter la valeur du champ, elle exporte la valeur d'une variable dynamique à portée limitée, transmise via @passOnwards(as: "...") ou @applyField(passOnwardsAs: "...").

Par exemple, dans cette requête, nous utilisons @applyField pour modifier les éléments du tableau et affecter cette nouvelle valeur à la variable dynamique à portée limitée $replaced. Ensuite, nous utilisons @exportFrom pour rendre cette valeur globalement accessible via la variable dynamique $replacedList, afin qu'elle puisse être récupérée depuis une requête ultérieure.

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

Cela produira :

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

@deferredExport

Lorsque la fonctionnalité Multi-Field Directives est activée et que nous exportons la valeur de plusieurs champs dans un dictionnaire, utilisez @deferredExport à la place de @export pour garantir que toutes les directives de chaque champ concerné ont été exécutées avant d'exporter la valeur du champ.

Par exemple, dans cette requête, le premier champ a la directive @strUpperCase appliquée, et le second a @strTitleCase. Lors de l'exécution de @deferredExport, la valeur exportée aura ces directives appliquées :

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

Produisant :

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

@skip et @include (dans les opérations)

Lorsque l'exécution de requêtes multiples est activée, les directives @include et @skip sont également disponibles en tant que directives d'opération, et elles peuvent être utilisées pour exécuter conditionnellement une opération si elle satisfait une condition.

Par exemple, dans cette requête, l'opération CheckIfPostExists exporte une variable dynamique $postExists et, uniquement si sa valeur est true, la mutation ExecuteOnlyIfPostExists sera exécutée :

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

Sorties de variables dynamiques

@export peut produire 6 sorties différentes, en fonction d'une combinaison de :

  • La valeur de l'argument type (soit SINGLE, LIST ou DICTIONARY)
  • Si la directive est appliquée à un seul champ, ou à plusieurs champs (via le module Multi-Field Directives)

Les 6 sorties possibles sont donc :

  1. Type SINGLE :
    1. Champ unique
    2. Multi-champ
  2. Type LIST :
    1. Champ unique
    2. Multi-champ
  3. Type DICTIONARY :
    1. Champ unique
    2. Multi-champ

Type SINGLE / Champ unique

La sortie est une valeur unique lorsqu'on passe le paramètre type: SINGLE (qui est défini comme valeur par défaut).

Dans cette requête :

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

...la variable dynamique $postTitle aura la valeur :

"Hello world!"

Notez que si SINGLE est appliqué sur un tableau d'entités, c'est la valeur de la dernière entité qui est exportée.

Dans cette requête :

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

...la variable dynamique $postTitle aura la valeur de l'article avec l'ID 5 :

"Everything good?"

Type SINGLE / Multi-champ

Si @export est appliqué sur plusieurs champs (en ajoutant le paramètre affectAdditionalFieldsUnderPos fourni par le module Multi-Field Directives), alors la valeur définie dans la variable dynamique est un dictionnaire de { key: field alias, value: field value } (de type JSONObject).

Cette requête :

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

...exporte la variable dynamique $postData avec la valeur :

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

Type LIST / Champ unique

La variable dynamique contiendra un tableau avec la valeur du champ de toutes les entités interrogées (du champ englobant), en passant le paramètre type: LIST.

Lors de l'exécution de cette requête (dans laquelle les entités interrogées sont des articles avec les ID 1 et 5) :

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

...la variable dynamique $postTitles aura la valeur :

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

Type LIST / Multi-champ

On obtient un tableau de dictionnaires (de type JSONObject), chacun contenant les valeurs des champs sur lesquels la directive est appliquée.

Cette requête :

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

...exporte la variable dynamique $postsData avec la valeur :

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

Type DICTIONARY / Champ unique

La variable dynamique contiendra un dictionnaire (de type JSONObject) avec l'ID de l'entité interrogée comme clé, et les valeurs du champ comme valeur, en passant le paramètre type: DICTIONARY.

Cette requête :

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

...exporte la variable dynamique $postIDTitles avec la valeur :

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

Type DICTIONARY / Multi-champ

Dans cette combinaison, nous exportons un dictionnaire de dictionnaires : { key: entity ID, value: { key: field alias, value: field value } } (en utilisant un type JSONObject qui contiendra des entrées de type JSONObject).

Cette requête :

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

...exporte la variable dynamique $postsIDProperties avec la valeur :

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

Export de valeurs lors de l'itération d'un tableau ou d'un objet JSON

@export respecte la cardinalité de toute méta-directive englobante.

En particulier, chaque fois que @export est imbriquée sous une méta-directive qui itère sur des éléments de tableau ou des propriétés d'objet JSON (c'est-à-dire @underEachArrayItem et @underEachJSONObjectProperty), la valeur exportée sera un tableau.

Cette requête :

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

...produit $contentAttributes avec la valeur :

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

En revanche, la même requête qui accède à un élément spécifique du tableau au lieu d'itérer sur tous (en remplaçant @underEachArrayItem par @underArrayItem(index: 0)) exportera une valeur unique.

Cette requête :

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

...produit $contentAttributes avec la valeur :

"List Block"

Ordre d'exécution des directives

S'il y a d'autres directives avant @export, la valeur exportée reflétera les modifications apportées par ces directives précédentes.

Par exemple, dans cette requête, selon que @export a lieu avant ou après @strUpperCase, le résultat sera différent :

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

Produisant :

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

Exécution dans les Persisted Queries

Lorsqu'une requête GraphQL contient plusieurs opérations dans une Persisted Query, nous pouvons invoquer l'endpoint correspondant en passant le paramètre URL ?operationName=... avec le nom de l'opération à exécuter ; sinon, la dernière opération sera exécutée.

Par exemple, pour exécuter l'opération GetPostsContainingString dans une Persisted Query avec l'endpoint /graphql-query/posts-with-user-name/, nous devons invoquer :

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

Exemples

Importer du contenu depuis un endpoint d'API externe :

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

Récupérer les données d'un article, les transformer et les stocker à nouveau :

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

Mettre à jour un article s'il existe, ou afficher un message d'erreur dans le cas contraire :

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
}

Connecter l'utilisateur avant d'exécuter une mutation, et le déconnecter immédiatement après :

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
}

Connecter conditionnellement l'utilisateur avant d'exécuter une mutation, si les identifiants sont fournis :

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
}

Spécification GraphQL

Cette fonctionnalité ne fait actuellement pas partie de la spécification GraphQL, mais elle a été demandée :