Spiegazione delle mutation annidate
Le mutation sono operazioni che possono modificare i dati sul server GraphQL, ad esempio durante la creazione di un articolo, l'aggiornamento del nome di un utente, l'aggiunta di un commento a un articolo, o altro.
In GraphQL, le mutation sono esposte unicamente sotto il tipo MutationRoot, in questo modo:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(Lo schema GraphQL in questa guida serve a illustrare gli esempi; è diverso dallo schema fornito dal plugin.)
Con questo schema, la modifica del nome dell'utente si effettua così:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Le mutation sono esposte unicamente nel mutation root object type per imporre che vengano eseguite in serie, come spiega la specifica 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.
Il termine "esecuzione seriale" si contrappone a "esecuzione parallela", che è peraltro il comportamento raccomandato per la risoluzione dei campi.
Ad esempio, nella query qui sotto, non importa quale campo (se name o email) il server GraphQL risolva per primo, e questi campi possono essere risolti in parallelo:
query {
user(by: { id: 37 }) {
name
email
}
}Le mutation modificano i dati, però, quindi l'ordine in cui i campi vengono risolti è importante, e perciò devono essere eseguite in serie (altrimenti potrebbero provocare race conditions).
Ad esempio, le due query qui sotto produrranno risultati diversi:
# Query 1: dopo l'esecuzione, il nome dell'utente sarà "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Query 2: dopo l'esecuzione, il nome dell'utente sarà "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}La conseguenza dell'esporre le mutation unicamente tramite MutationRoot è che questo tipo diventa molto ingombrante, contenendo campi che non hanno nulla in comune tra loro se non il dover essere eseguiti in serie (il che è una questione tecnica, e non una decisione di progettazione dell'interfaccia).
Il caso a favore delle mutation annidate
Tra le mutation qui sopra, solo createPost vive veramente sotto il tipo MutationRoot, perché crea un nuovo elemento dal nulla. Le mutation updateUserName e addCommentToPost, invece, possono perfettamente avere operazioni equivalenti applicate su un'entità esistente di un altro tipo:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}Con questo schema, la modifica del nome dell'utente potrebbe essere realizzata così:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}Questa funzionalità si chiama "nested mutations": applicare una mutation al risultato di un'altra operazione, che si tratti di una query o di una mutation.
Nota come l'utilizzo delle mutation annidate renda lo schema GraphQL più elegante:
- Mentre l'operazione
MutationRoot.updateUserNamedeve ricevere l'IDdell'utente, la sua operazione equivalenteUser.updateNamenon ne ha bisogno, perché viene già eseguita su un'entità utente - Il nome del campo viene abbreviato da
updateUserNameaupdateName
Inoltre, il servizio GraphQL diventa più semplice e più comprensibile, perché possiamo navigare tra le entità del grafo per modificare i loro dati allo stesso modo in cui li interroghiamo.
Le mutation annidate possono scendere su più livelli. Ad esempio, possiamo aggiungere un commento a un articolo appena creato, il tutto in una singola query:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}Da qui, le mutation annidate possono anche migliorare le prestazioni riducendo la latenza degli andata e ritorno, passando dall'esecuzione di più query per mutare diversi elementi all'esecuzione di una singola query.
Perché le mutation annidate non fanno parte della specifica
La specifica GraphQL è concepita per funzionare con tutte le implementazioni di server GraphQL per qualsiasi linguaggio. Tuttavia, la sua forza motrice è JavaScript tramite graphql-js, l'implementazione di riferimento.
In altre parole, qualsiasi funzionalità che non possa essere supportata da graphql-js non farà parte della specifica.
Poiché JavaScript supporta le promises, la risoluzione parallela dei campi era realizzabile, e il parallelismo è diventato uno dei principi fondamentali nella progettazione iniziale di graphql-js, come testimonia DataLoader (il livello di recupero dei dati), le cui funzioni di batching restituiscono JavaScript promises.
I vantaggi dell'esecuzione parallela per le prestazioni sono troppo numerosi, e le mutation annidate non possono funzionare con il parallelismo. È stato deciso che lo scambio dell'esecuzione parallela con le mutation annidate non ne varrebbe la pena.
Mutation annidate e prestazioni
Per il plugin Gato GraphQL, i campi vengono sempre risolti in serie, e l'ordine in cui vengono risolti è deterministico. (Questa caratteristica non incide sulle prestazioni di risoluzione della query, perché il server trasforma prima il grafo della query in un modello a componenti, che viene risolto in un tempo lineare ottimale).
Il che significa che il plugin può supportare le mutation annidate, beneficiando di tutti i loro vantaggi, senza subirne gli svantaggi.
Specifica GraphQL
Questa funzionalità attualmente non fa parte della specifica GraphQL, ma è stata richiesta in: