Erklärung der verschachtelten Mutations
Mutations sind Operationen, die Daten auf dem GraphQL-Server verändern können, zum Beispiel beim Erstellen eines Beitrags, beim Aktualisieren des Benutzernamens, beim Hinzufügen eines Kommentars zu einem Beitrag oder Ähnlichem.
In GraphQL werden Mutations ausschließlich unter dem Typ MutationRoot bereitgestellt, wie folgt:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(Das GraphQL-Schema in dieser Anleitung dient der Veranschaulichung der Beispiele; es unterscheidet sich vom Schema des Plugins.)
Mit diesem Schema wird der Benutzername folgendermaßen geändert:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Mutations werden nur im Mutation-Root-Object-Typ bereitgestellt, um sicherzustellen, dass sie seriell ausgeführt werden, wie in der GraphQL-Spezifikation erläutert:
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.
Der Begriff „serielle Ausführung" steht im Gegensatz zur „parallelen Ausführung", die ansonsten das empfohlene Verhalten bei der Auflösung von Feldern ist.
In der folgenden Query beispielsweise spielt es keine Rolle, welches Feld (ob name oder email) der GraphQL-Server zuerst auflöst – diese können parallel aufgelöst werden:
query {
user(by: { id: 37 }) {
name
email
}
}Mutations verändern jedoch Daten, daher ist die Reihenfolge, in der Felder aufgelöst werden, wichtig – sie müssen seriell ausgeführt werden (andernfalls könnten sie Race Conditions verursachen).
Die beiden folgenden queries werden zum Beispiel unterschiedliche Ergebnisse liefern:
# Query 1: after execution, user name will be "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Query 2: after execution, user name will be "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}Die Konsequenz daraus, Mutations nur über MutationRoot bereitzustellen, ist, dass dieser Typ sehr aufgebläht wird und Felder enthält, die nichts gemeinsam haben außer der Tatsache, dass sie seriell ausgeführt werden müssen (was eine technische Angelegenheit ist und keine Entscheidung im Interface-Design).
Das Argument für verschachtelte Mutations
Von den obigen Mutations lebt nur createPost wirklich unter dem Typ MutationRoot, weil es ein neues Element aus dem Nichts erzeugt. Die Mutations updateUserName und addCommentToPost können hingegen problemlos äquivalente Operationen haben, die auf eine bestehende Entität eines anderen Typs angewendet werden:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}Mit diesem Schema könnte die Änderung des Benutzernamens so erreicht werden:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}Diese Funktion heißt „nested mutations": eine Mutation auf das Ergebnis einer anderen Operation anwenden, ob es sich um eine Query oder eine Mutation handelt.
Beachte, wie der Einsatz von verschachtelten Mutations das GraphQL-Schema eleganter macht:
- Während die Operation
MutationRoot.updateUserNamedieIDdes Benutzers erhalten muss, benötigt die äquivalente OperationUser.updateNamediese nicht, da sie bereits auf einer Benutzerentität ausgeführt wird - Der Feldname wird von
updateUserNamezuupdateNameverkürzt
Außerdem wird der GraphQL-Dienst einfacher und verständlicher, da wir zwischen Entitäten im Graphen navigieren können, um ihre Daten auf dieselbe Weise zu verändern, wie wir sie abfragen.
Verschachtelte Mutations können mehrere Ebenen tief gehen. Zum Beispiel können wir einem neu erstellten Beitrag einen Kommentar hinzufügen – alles innerhalb einer einzigen Query:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}Dadurch können verschachtelte Mutations auch die Leistung verbessern, indem sie die Roundtrip-Latenz reduzieren – von der Ausführung mehrerer queries zum Mutieren verschiedener Elemente hin zur Ausführung einer einzigen Query.
Warum verschachtelte Mutations nicht Teil der Spezifikation sind
Die GraphQL-Spezifikation ist darauf ausgelegt, für alle Implementierungen von GraphQL-Servern in jeder Sprache zu funktionieren. Ihre treibende Kraft ist jedoch JavaScript über graphql-js, die Referenzimplementierung.
Mit anderen Worten: Jede Funktion, die von graphql-js nicht unterstützt werden kann, wird nicht Teil der Spezifikation sein.
Da JavaScript Promises unterstützt, war die parallele Auflösung von Feldern machbar, und Parallelismus wurde zu einem der Grundprinzipien beim ersten Entwurf von graphql-js, wie DataLoader (die Datenabrufschicht) zeigt, dessen Batching-Funktionen JavaScript-Promises zurückgeben.
Die Vorteile der parallelen Ausführung für die Leistung sind zu zahlreich, und verschachtelte Mutations können nicht mit Parallelismus funktionieren. Es wurde entschieden, dass es sich nicht lohnen würde, die parallele Ausführung gegen verschachtelte Mutations einzutauschen.
Verschachtelte Mutations und Leistung
Beim Plugin Gato GraphQL werden Felder immer seriell aufgelöst, und die Reihenfolge, in der sie aufgelöst werden, ist deterministisch. (Diese Eigenschaft beeinträchtigt die Leistung der Query-Auflösung nicht, da der Server den Graphen in der Query zunächst in ein Komponentenmodell umwandelt, das in optimaler linearer Zeit aufgelöst wird.)
Das bedeutet, dass das Plugin verschachtelte Mutations unterstützen kann, alle Vorteile davon nutzt und keinen der Nachteile in Kauf nehmen muss.
GraphQL-Spezifikation
Diese Funktionalität ist derzeit nicht Teil der GraphQL-Spezifikation, wurde aber beantragt in: