Explicando las mutaciones anidadas
Las mutaciones son operaciones que pueden alterar datos en el servidor GraphQL, como al crear una entrada, actualizar el nombre del usuario, añadir un comentario a una entrada, u otras.
En GraphQL, las mutaciones se exponen únicamente bajo el tipo MutationRoot, así:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(El esquema GraphQL en esta guía es para ilustrar los ejemplos; es diferente del esquema proporcionado en el plugin.)
Con este esquema, modificar el nombre del usuario se consigue así:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Las mutaciones se exponen en el mutation root object type únicamente para imponer que se ejecuten en serie, como explica la especificación 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.
El término "ejecución serial" se opone a "ejecución paralela", que es por lo demás el comportamiento recomendado para resolver campos.
Por ejemplo, en la consulta de abajo, no importa qué campo (si name o email) resuelve primero el servidor GraphQL, y estos pueden resolverse en paralelo:
query {
user(by: { id: 37 }) {
name
email
}
}Sin embargo, las mutaciones alteran datos, por lo que el orden en que se resuelven los campos sí importa, y por tanto deben ejecutarse en serie (o, de lo contrario, podrían producir race conditions).
Por ejemplo, las dos consultas de abajo producirán resultados diferentes:
# Consulta 1: tras la ejecución, el nombre de usuario será "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Consulta 2: tras la ejecución, el nombre de usuario será "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}La consecuencia de exponer las mutaciones solo mediante MutationRoot es que este tipo se vuelve muy abultado, conteniendo campos sin nada en común entre sí más allá de tener que ejecutarse en serie (lo cual es una cuestión técnica, no una decisión de diseño de interfaz).
El caso para las mutaciones anidadas
De las mutaciones anteriores, solo createPost vive realmente bajo el tipo MutationRoot, porque está creando un nuevo elemento de la nada. Las mutaciones updateUserName y addCommentToPost, sin embargo, pueden perfectamente tener operaciones equivalentes aplicadas sobre una entidad existente de otro tipo:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}Con este esquema, modificar el nombre del usuario podría conseguirse así:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}Esta característica se llama "nested mutations": aplicar una mutación al resultado de otra operación, ya sea consulta o mutación.
Por favor, fíjate cómo usar mutaciones anidadas hace el esquema GraphQL más elegante:
- Mientras que la operación
MutationRoot.updateUserNamedebe recibir elIDdel usuario, su operación equivalenteUser.updateNameno debe hacerlo, ya que se ejecuta sobre una entidad de usuario - El nombre del campo se acorta de
updateUserNameaupdateName
Además, el servicio GraphQL se vuelve más simple y comprensible, ya que podemos navegar entre entidades en el grafo para modificar sus datos de la misma forma que para consultar sus datos.
Las mutaciones anidadas pueden bajar múltiples niveles. Por ejemplo, podemos añadir un comentario en una entrada recién creada, todo dentro de una única consulta:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}A partir de esto, las mutaciones anidadas también pueden mejorar el rendimiento reduciendo la latencia de ida y vuelta, pasando de ejecutar múltiples consultas para mutar varios elementos, a ejecutar una única consulta.
Por qué las mutaciones anidadas no forman parte de la spec
La especificación GraphQL está pensada para funcionar para todas las implementaciones de servidores GraphQL para cualquier lenguaje. Sin embargo, su fuerza motriz es JavaScript a través de graphql-js, la implementación de referencia.
En otras palabras, cualquier característica que no pueda ser soportada por graphql-js no formará parte de la especificación.
Como JavaScript soporta promises, la resolución paralela de campos era factible, y el paralelismo se convirtió en uno de los principios fundamentales al diseñar inicialmente graphql-js, como se manifiesta en DataLoader (la capa de obtención de datos), cuyas funciones de batching devuelven JavaScript promises.
Las ventajas de la ejecución paralela para el rendimiento son demasiadas, y las mutaciones anidadas no pueden funcionar con paralelismo. Se ha decidido que no valdría la pena intercambiar la ejecución paralela por mutaciones anidadas.
Mutaciones anidadas y rendimiento
Para el plugin Gato GraphQL, los campos siempre se resuelven en serie, y el orden en que se resuelven es determinista. (Esta característica no afecta al rendimiento de resolución de la consulta, ya que el servidor primero transforma el grafo de la consulta en un component model, que se resuelve en un tiempo lineal óptimo).
Lo que significa que el plugin puede soportar mutaciones anidadas, proporcionando todos sus beneficios, y no sufriendo ninguna de sus consecuencias.
Especificación de GraphQL
Esta funcionalidad no forma parte actualmente de la especificación GraphQL, pero ha sido solicitada en: