Concepten, Ideeƫn, Strategieƫn
Concepten, Ideeƫn, StrategieƫnUitleg over geneste mutations

Uitleg over geneste mutations

Mutations zijn operaties die gegevens op de GraphQL-server kunnen wijzigen, zoals het aanmaken van een post, het bijwerken van de naam van een gebruiker, het toevoegen van een reactie aan een post, of andere acties.

In GraphQL worden mutations uitsluitend beschikbaar gesteld via het type MutationRoot, als volgt:

type MutationRoot {
  createPost(id: ID!, title: String!, content: String): Post!
  updateUserName(userID: ID!, newName: String!): User!
  addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}

(Het GraphQL-schema in deze gids dient ter illustratie van de voorbeelden; het verschilt van het schema dat door de plugin wordt geleverd.)

Met dit schema wordt het wijzigen van de naam van een gebruiker als volgt gedaan:

mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Mutations worden uitsluitend beschikbaar gesteld via het mutation root object type om te garanderen dat ze serieel worden uitgevoerd, zoals uitgelegd in de GraphQL-specificatie:

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.

De term "seriƫle uitvoering" staat tegenover "parallelle uitvoering", wat overigens het aanbevolen gedrag is bij het oplossen van velden.

In de onderstaande query maakt het bijvoorbeeld niet uit welk veld (name of email) de GraphQL-server als eerste oplost — deze kunnen parallel worden opgelost:

query {
  user(by: { id: 37 }) {
    name
    email
  }
}

Mutations wijzigen echter gegevens, dus de volgorde waarin velden worden opgelost is wel belangrijk — ze moeten serieel worden uitgevoerd (anders kunnen ze race conditions veroorzaken).

De twee onderstaande queries leveren bijvoorbeeld verschillende resultaten op:

# Query 1: na uitvoering is de gebruikersnaam "John"
mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
  updateUserName(userID: 37, newName: "John") {
    name
  }
}
 
# Query 2: na uitvoering is de gebruikersnaam "Peter"
mutation {
  updateUserName(userID: 37, newName: "John") {
    name
  }
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Het gevolg van het uitsluitend beschikbaar stellen van mutations via MutationRoot is dat dit type zwaar overbelast raakt, met velden die onderling niets gemeen hebben behalve dat ze serieel moeten worden uitgevoerd (wat een technische kwestie is, geen ontwerpbeslissing voor de interface).

Het argument voor geneste mutations

Van de bovenstaande mutations hoort alleen createPost echt thuis onder het type MutationRoot, omdat het een nieuw element van nul af aan aanmaakt. Mutations updateUserName en addCommentToPost kunnen echter prima equivalente operaties hebben die worden toegepast op een bestaande entiteit van een ander type:

type User {
  updateName(newName: String!): User!
}
 
type Post {
  addComment(comment: String!, userID: ID): Comment!
}

Met dit schema zou het wijzigen van de naam van een gebruiker als volgt kunnen worden gedaan:

mutation {
  user(ID: 37) {
    updateName(newName: "Peter") {
      name
    }
  }
}

Deze functie heet "geneste mutations": het toepassen van een mutation op het resultaat van een andere operatie, of dat nu een query of een mutation is.

Let op hoe het gebruik van geneste mutations het GraphQL-schema eleganter maakt:

  • Terwijl operatie MutationRoot.updateUserName het ID van de gebruiker moet ontvangen, hoeft het equivalent User.updateName dat niet, omdat het al wordt uitgevoerd op een gebruikersentiteit
  • De veldnaam wordt ingekort van updateUserName naar updateName

Bovendien wordt de GraphQL-service eenvoudiger en begrijpelijker, omdat je door entiteiten in de graph kunt navigeren om hun gegevens te wijzigen op dezelfde manier als waarop je ze opvraagt.

Geneste mutations kunnen meerdere niveaus diep gaan. We kunnen bijvoorbeeld een reactie toevoegen aan een nieuw aangemaakte post, alles binnen ƩƩn query:

mutation {
  createPost(ID: 37, title: "Hello world!", content: "Just another post") {
    id
    addComment(comment: "Lovely post") {
      id
    }
  }
}

Hierdoor kunnen geneste mutations ook de prestaties verbeteren door de latentie van meerdere netwerkreizen te verminderen — van het uitvoeren van meerdere queries om verschillende elementen te muteren, naar het uitvoeren van ƩƩn enkele query.

Waarom geneste mutations geen deel uitmaken van de specificatie

De GraphQL-specificatie is bedoeld om te werken voor alle implementaties van GraphQL-servers voor elke programmeertaal. De drijvende kracht erachter is echter JavaScript via graphql-js, de referentie-implementatie.

Met andere woorden: elke functie die niet door graphql-js kan worden ondersteund, zal geen deel uitmaken van de specificatie.

Omdat JavaScript promises ondersteunt, was parallelle oplossing van velden haalbaar, en parallellisme werd een van de fundamentele principes bij het eerste ontwerp van graphql-js, zoals blijkt uit DataLoader (de data-ophaallaag), waarvan de batchingfuncties JavaScript-promises retourneren.

De voordelen van parallelle uitvoering voor prestaties zijn enorm, en geneste mutations kunnen niet werken met parallellisme. Er is besloten dat het niet de moeite waard is om parallelle uitvoering in te ruilen voor geneste mutations.

Geneste mutations en prestaties

In plugin Gato GraphQL worden velden altijd serieel opgelost, en de volgorde waarin ze worden opgelost is deterministisch. (Dit kenmerk heeft geen invloed op de prestaties van de query-oplossing, omdat de server de graph in de query eerst omzet in een componentmodel, dat in optimale lineaire tijd wordt opgelost.)

Dit betekent dat de plugin geneste mutations kan ondersteunen, met alle voordelen ervan, zonder de nadelen.

GraphQL-specificatie

Deze functionaliteit maakt momenteel geen deel uit van de GraphQL-specificatie, maar is aangevraagd in: