Expliquer les mutations imbriquées
Les mutations sont des opérations qui peuvent modifier des données sur le serveur GraphQL, par exemple lors de la création d'un article, de la mise à jour du nom d'un utilisateur, de l'ajout d'un commentaire à un article, ou autre.
En GraphQL, les mutations sont exposées uniquement sous le type MutationRoot, comme ceci :
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(Le schéma GraphQL dans ce guide sert à illustrer les exemples ; il est différent du schéma fourni par le plugin.)
Avec ce schéma, la modification du nom de l'utilisateur s'effectue ainsi :
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Les mutations sont exposées uniquement dans le mutation root object type afin d'imposer qu'elles soient exécutées en série, comme l'explique la spécification GraphQL :
It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.
Le terme « exécution série » s'oppose à « exécution parallèle », qui est par ailleurs le comportement recommandé pour la résolution des champs.
Par exemple, dans la requête ci-dessous, peu importe quel champ (si name ou email) le serveur GraphQL résout en premier, et ces champs peuvent être résolus en parallèle :
query {
user(by: { id: 37 }) {
name
email
}
}Les mutations modifient des données, toutefois, donc l'ordre dans lequel les champs sont résolus a de l'importance, et elles doivent donc être exécutées en série (faute de quoi, elles pourraient provoquer des race conditions).
Par exemple, les deux requêtes ci-dessous produiront des résultats différents :
# Requête 1 : après exécution, le nom de l'utilisateur sera "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Requête 2 : après exécution, le nom de l'utilisateur sera "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}La conséquence d'exposer les mutations uniquement via MutationRoot est que ce type devient très encombré, contenant des champs qui n'ont rien en commun entre eux si ce n'est devoir être exécutés en série (ce qui est une question technique, et non une décision de conception d'interface).
Le cas en faveur des mutations imbriquées
Parmi les mutations ci-dessus, seule createPost vit véritablement sous le type MutationRoot, car elle crée un nouvel élément à partir de rien. Les mutations updateUserName et addCommentToPost, en revanche, peuvent parfaitement avoir des opérations équivalentes appliquées sur une entité existante d'un autre type :
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}Avec ce schéma, la modification du nom de l'utilisateur pourrait être réalisée ainsi :
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}Cette fonctionnalité s'appelle « nested mutations » : appliquer une mutation au résultat d'une autre opération, qu'il s'agisse d'une requête ou d'une mutation.
Remarquez comment l'utilisation des mutations imbriquées rend le schéma GraphQL plus élégant :
- Alors que l'opération
MutationRoot.updateUserNamedoit recevoir l'IDde l'utilisateur, son opération équivalenteUser.updateNamen'en a pas besoin, car elle est déjà exécutée sur une entité utilisateur - Le nom du champ est raccourci de
updateUserNameàupdateName
De plus, le service GraphQL devient plus simple et plus compréhensible, car nous pouvons naviguer entre les entités du graphe pour modifier leurs données de la même façon que pour interroger leurs données.
Les mutations imbriquées peuvent descendre sur plusieurs niveaux. Par exemple, nous pouvons ajouter un commentaire sur un article nouvellement créé, le tout dans une seule requête :
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}À partir de là, les mutations imbriquées peuvent également améliorer les performances en réduisant la latence des allers-retours, en passant de l'exécution de plusieurs requêtes pour muter plusieurs éléments à l'exécution d'une seule requête.
Pourquoi les mutations imbriquées ne font pas partie de la spécification
La spécification GraphQL est conçue pour fonctionner avec toutes les implémentations de serveurs GraphQL pour n'importe quel langage. Cependant, sa force motrice est JavaScript via graphql-js, l'implémentation de référence.
En d'autres termes, toute fonctionnalité qui ne peut pas être prise en charge par graphql-js ne fera pas partie de la spécification.
Puisque JavaScript prend en charge les promises, la résolution parallèle des champs était réalisable, et le parallélisme est devenu l'un des principes fondamentaux lors de la conception initiale de graphql-js, comme en témoigne DataLoader (la couche de récupération de données), dont les fonctions de batching retournent des JavaScript promises.
Les avantages de l'exécution parallèle pour les performances sont trop nombreux, et les mutations imbriquées ne peuvent pas fonctionner avec le parallélisme. Il a été décidé que l'échange de l'exécution parallèle contre les mutations imbriquées n'en vaudrait pas la peine.
Mutations imbriquées et performances
Pour le plugin Gato GraphQL, les champs sont toujours résolus en série, et l'ordre dans lequel ils sont résolus est déterministe. (Cette caractéristique n'affecte pas les performances de résolution de la requête, car le serveur transforme d'abord le graphe de la requête en un modèle de composant, qui est résolu en un temps linéaire optimal).
Ce qui signifie que le plugin peut prendre en charge les mutations imbriquées, en bénéficiant de tous leurs avantages, sans en subir les inconvénients.
Spécification GraphQL
Cette fonctionnalité ne fait actuellement pas partie de la spécification GraphQL, mais elle a été demandée dans :