Configurer le schéma
Configurer le schémaExécuter plusieurs requêtes concurremment

Exécuter plusieurs requêtes concurremment

Plusieurs requêtes peuvent être combinées et exécutées comme une seule opération, en réutilisant leur état et leurs données.

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

Cette fonctionnalité améliore les performances. Au lieu d'exécuter des requêtes indépendamment dans différentes demandes (de sorte que nous exécutons d'abord une opération contre le serveur GraphQL, puis attendons sa réponse, puis utilisons ce résultat pour effectuer une autre opération), nous pouvons les exécuter ensemble, évitant ainsi la latence liée aux multiples demandes.

L'exécution de plusieurs requêtes (Multiple Query Execution) nous permet également de mieux organiser nos requêtes GraphQL, en les divisant en unités logiques qui dépendent les unes des autres, et qui sont exécutées conditionnellement en fonction du résultat d'une opération précédente.

Comment utiliser l'exécution de plusieurs requêtes

Supposons que nous voulions rechercher tous les posts qui mentionnent le nom de l'utilisateur connecté. Normalement, nous aurions besoin de deux requêtes pour accomplir cela :

Nous récupérons d'abord le name de l'utilisateur :

query GetLoggedInUserName {
  me {
    name
  }
}

...et ensuite, après avoir exécuté la première requête, nous pouvons passer le name de l'utilisateur récupéré comme variable $search pour effectuer la recherche dans une deuxième requête :

query GetPostsContainingString($search: String!) {
  posts(filter: { search: $search }) {
    id
    title
  }
}

Multiple Query Execution simplifie ce processus, nous permettant de récupérer toutes les données et d'exécuter toute la logique requise en une seule demande :

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

L'exécution de plusieurs requêtes (Multiple Query Execution) est obtenue grâce à ces directives spéciales :

  • @depends (directive d'opération) : faire en sorte 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) : exporter la valeur d'un champ d'une opération, pour l'injecter comme entrée dans un champ d'une autre opération
  • @deferredExport (directive de champ) : similaire à @export mais à utiliser avec 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 certaine condition.

Le serveur GraphQL créera la liste des opérations à charger et à exécuter, en les récupérant de chaque @depends(on: ...), et exportera les valeurs de tout champ contenant @export comme variable dynamique (avec le nom défini sous l'argument as) pour être utilisées en entrée dans toute opération ultérieure.

En combinant ces directives, nous sommes en mesure de diviser toute fonctionnalité complexe en étapes intermédiaires, en alternant les opérations query et mutation, d'ajouter leurs dépendances dans l'ordre requis, et de les exécuter toutes en une seule demande en définissant l'opération la plus externe dans ?operationName=... (dans l'exemple ci-dessus, ce sera ?operationName=GetPostsContainingString).

Définir les opérations à charger et à exécuter via @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érations ([String]), ou nous pouvons é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 ...
}

Partager des données entre requêtes via @export

La directive @export exporte la valeur d'un champ (ou d'un ensemble de champs) dans une variable dynamique, pour être utilisée comme entrée dans un champ d'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 posts 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
  }
}

Sorties des variables dynamiques

@export peut produire 6 sorties différentes, basées sur 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 en passant 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, alors 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 du post 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: alias du champ, value: valeur du champ } (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 posts avec les IDs 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

Nous obtenons 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: ID de l'entité, value: { key: alias du champ, value: valeur du champ } } (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."
  }
}

Exécution conditionnelle des opérations

Lorsque Multiple Query Execution est activé, les directives @include et @skip sont également disponibles en tant que directives d'opération, et peuvent être utilisées pour exécuter conditionnellement une opération si elle satisfait une certaine condition.

Par exemple, dans cette requête, l'opération CheckIfPostExists exporte une variable dynamique $postExists et, seulement 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...
}

Exporter des 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 les éléments d'un tableau ou les propriétés d'un 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"
  }
}

Multi-Field Directives

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 impliqué 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 @titleCase. 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 @titleCase # 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"
    }
  }
}

Spec GraphQL

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