嵌套 mutation 详解
Mutation 是可以修改 GraphQL 服务器上数据的操作,例如创建文章、更新用户名、为文章添加评论等。
在 GraphQL 中,mutation 仅通过 MutationRoot 类型公开,如下所示:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(本指南中的 GraphQL schema 仅用于说明示例,与插件提供的 schema 不同。)
使用此 schema,修改用户名的方式如下:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Mutation 仅通过 mutation root object type 公开,是为了强制串行执行,正如 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.
「串行执行」与「并行执行」相对,并行执行是字段解析的推荐行为。
例如,在以下 query 中,GraphQL 服务器先解析哪个字段(name 还是 email)并不重要,它们可以并行解析:
query {
user(by: { id: 37 }) {
name
email
}
}然而,mutation 会修改数据,因此字段的解析顺序至关重要,必须串行执行(否则可能产生竞态条件)。
例如,以下两个 query 会产生不同的结果:
# Query 1: 执行后,用户名将为 "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Query 2: 执行后,用户名将为 "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}仅通过 MutationRoot 公开 mutation 的结果是,该类型变得极为臃肿,包含除必须串行执行(这是技术问题,而非接口设计决策)之外毫无共同点的字段。
嵌套 mutation 的必要性
在上述 mutation 中,只有 createPost 真正属于 MutationRoot 类型,因为它是凭空创建新元素。而 updateUserName 和 addCommentToPost 则完全可以在另一类型的现有实体上执行等效操作:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}使用此 schema,修改用户名可以这样实现:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}这一功能称为「嵌套 mutation」:将 mutation 应用于另一个操作(无论是 query 还是 mutation)的结果。
请注意,使用嵌套 mutation 如何使 GraphQL schema 更加优雅:
MutationRoot.updateUserName操作必须接收用户的ID,而等效的User.updateName操作则不需要,因为它已在用户实体上执行- 字段名从
updateUserName缩短为updateName
此外,由于我们可以在图中的实体之间导航,以与查询数据相同的方式修改数据,GraphQL 服务变得更简单、更易于理解。
嵌套 mutation 可以深入多个层级。例如,可以在单个 query 中为新创建的文章添加评论:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}由此,嵌套 mutation 还可以通过减少往返延迟来提升性能——从执行多个 query 来 mutate 多个元素,变为执行单个 query。
嵌套 mutation 未纳入规范的原因
GraphQL 规范旨在适用于任何语言的所有 GraphQL 服务器实现。然而,其主要推动力是通过 graphql-js(参考实现)实现的 JavaScript。
换句话说,graphql-js 无法支持的功能不会成为规范的一部分。
由于 JavaScript 支持 promises,字段的并行解析成为可行,并行性成为最初设计 graphql-js 时的基本原则之一,这在 DataLoader(数据获取层)中有所体现——其批处理函数返回 JavaScript promises。
并行执行在性能上的优势不胜枚举,而嵌套 mutation 无法与并行性共存。因此决定,以并行执行换取嵌套 mutation 并不值得。
嵌套 mutation 与性能
在 Gato GraphQL 插件中,字段始终串行解析,解析顺序是确定性的。(这一特性不会影响 query 解析性能,因为服务器首先将 query 中的图转换为组件模型,再以最优线性时间解析它。)
这意味着插件可以支持嵌套 mutation,提供其所有优势,同时不受其任何负面影响。
GraphQL 规范
此功能目前不是 GraphQL 规范的一部分,但已在以下地方提出请求: