Explicação das mutations aninhadas
As mutations são operações que podem alterar dados no servidor GraphQL, como ao criar um post, atualizar o nome do usuário, adicionar um comentário a um post, entre outros.
No GraphQL, as mutations são expostas apenas sob o tipo MutationRoot, desta forma:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(O schema GraphQL neste guia serve para ilustrar os exemplos; é diferente do schema fornecido pelo plugin.)
Com este schema, a modificação do nome do usuário é feita assim:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}As mutations são expostas apenas no mutation root object type para garantir que sejam executadas em série, conforme explicado na especificação 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.
O termo "execução serial" se opõe a "execução paralela", que é o comportamento recomendado para a resolução de campos.
Por exemplo, na query abaixo, não importa qual campo (se name ou email) o servidor GraphQL resolve primeiro, e esses campos podem ser resolvidos em paralelo:
query {
user(by: { id: 37 }) {
name
email
}
}As mutations alteram dados, porém, então a ordem em que os campos são resolvidos importa, e por isso devem ser executadas em série (caso contrário, poderiam produzir race conditions).
Por exemplo, as duas queries abaixo produzirão resultados diferentes:
# Query 1: após a execução, o nome do usuário será "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Query 2: após a execução, o nome do usuário será "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}A consequência de expor as mutations apenas por meio do MutationRoot é que esse tipo se torna muito sobrecarregado, contendo campos que não têm nada em comum entre si, exceto precisar ser executados em série (o que é uma questão técnica, não uma decisão de design de interface).
O caso a favor das mutations aninhadas
Entre as mutations acima, apenas createPost realmente pertence ao tipo MutationRoot, pois está criando um novo elemento do nada. As mutations updateUserName e addCommentToPost, porém, podem perfeitamente ter operações equivalentes aplicadas sobre uma entidade existente de outro tipo:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}Com este schema, a modificação do nome do usuário poderia ser feita assim:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}Esse recurso é chamado de "mutations aninhadas": aplicar uma mutation ao resultado de outra operação, seja uma query ou uma mutation.
Observe como o uso de mutations aninhadas torna o schema GraphQL mais elegante:
- Enquanto a operação
MutationRoot.updateUserNamedeve receber oIDdo usuário, sua operação equivalenteUser.updateNamenão precisa, pois já é executada sobre uma entidade de usuário - O nome do campo é encurtado de
updateUserNameparaupdateName
Além disso, o serviço GraphQL se torna mais simples e mais compreensível, pois podemos navegar entre as entidades do grafo para modificar seus dados da mesma forma que os consultamos.
As mutations aninhadas podem descer por vários níveis. Por exemplo, podemos adicionar um comentário a um post recém-criado, tudo dentro de uma única query:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}A partir disso, as mutations aninhadas também podem melhorar o desempenho ao reduzir a latência de ida e volta, passando da execução de múltiplas queries para mutar vários elementos para a execução de uma única query.
Por que as mutations aninhadas não fazem parte da especificação
A especificação GraphQL foi concebida para funcionar com todas as implementações de servidores GraphQL em qualquer linguagem. No entanto, sua força motriz é o JavaScript por meio do graphql-js, a implementação de referência.
Em outras palavras, qualquer recurso que não possa ser suportado pelo graphql-js não fará parte da especificação.
Como o JavaScript suporta promises, a resolução paralela de campos era viável, e o paralelismo se tornou um dos princípios fundamentais no design inicial do graphql-js, conforme evidenciado pelo DataLoader (a camada de busca de dados), cujas funções de batching retornam JavaScript promises.
As vantagens da execução paralela para o desempenho são muitas, e as mutations aninhadas não podem funcionar com paralelismo. Foi decidido que não valeria a pena trocar a execução paralela pelas mutations aninhadas.
Mutations aninhadas e desempenho
Para o plugin Gato GraphQL, os campos são sempre resolvidos em série, e a ordem em que são resolvidos é determinística. (Essa característica não afeta o desempenho de resolução da query, pois o servidor primeiro transforma o grafo da query em um modelo de componentes, que é resolvido em tempo linear otimizado).
O que significa que o plugin pode suportar mutations aninhadas, aproveitando todos os seus benefícios, sem sofrer nenhuma de suas consequências.
Especificação GraphQL
Essa funcionalidade atualmente não faz parte da especificação GraphQL, mas foi solicitada em: