Mutation:新增、更新、删除
🎯 引言
到目前为止,我们的接口只能读数据。实际项目里,注册账号、修改资料、删除记录这些写操作同样重要。GraphQL 里负责写操作的就是 Mutation。
学完这篇文章,你能说清楚 Query 和 Mutation 的分工,学会用 input 类型组织复杂入参,并动手实现用户数据的新增、更新、删除三个完整操作,让接口真正读写双全。
🧱 Mutation 是什么
Mutation 是 GraphQL 中用于修改数据的操作类型,地位上和 Query 平级。分工很明确:
- Query:只读,查多少次都不该改变数据,类似 REST 的 GET。
- Mutation:会改变数据,涵盖 REST 里 POST、PUT、DELETE 干的事。
写法上两者几乎一样,只是最外层的关键字从 query 换成 mutation:
mutation {
createUser(input: { username: "阿伟", age: 25 }) {
id
username
}
}
注意 Mutation 也要声明返回字段。这是个很实用的设计:创建或修改完成后,服务端把最新数据按你指定的字段返回,客户端不用再发一次查询去刷新。
🛠 input 输入类型
创建用户要传好几个字段,如果平铺成参数,声明会越来越长:
# 参数一多就不好维护了
type Mutation {
createUser(username: String!, age: Int, city: String): User!
}
GraphQL 为此提供了 input 类型:把一组入参打包成一个输入对象。
input CreateUserInput {
username: String!
age: Int
city: String
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
input 和 type 的写法几乎一样,区别在于 input 专门用于入参,type 用于出参,两者不能混用。这样做的好处:
- 入参结构集中管理,字段增减只改一处。
- 配合变量传参特别整齐,一个 JSON 对象搞定:
{
"input": {
"username": "阿伟",
"age": 25
}
}
mutation Create($input: CreateUserInput!) {
createUser(input: $input) {
id
username
}
}
✨ 完整的增删改实现
把三个操作一次实现完整。Schema 部分:
input CreateUserInput {
username: String!
age: Int
city: String
}
input UpdateUserInput {
username: String
age: Int
city: String
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
deleteUser(id: ID!): Boolean!
}
注意几个设计细节:
UpdateUserInput的字段全部可为空:更新时只传要改的字段,没传的不动。updateUser返回User(可空):找不到目标时返回null,客户端据此提示「用户不存在」。deleteUser返回Boolean!:用true/false明确表示删除成功与否。
resolvers 部分:
const resolvers = {
Mutation: {
createUser: (parent, args) => {
const user = {
id: String(users.length + 1),
...args.input,
};
users.push(user);
return user;
},
updateUser: (parent, args) => {
const user = users.find((item) => item.id === args.id);
if (!user) {
return null;
}
Object.assign(user, args.input);
return user;
},
deleteUser: (parent, args) => {
const index = users.findIndex((item) => item.id === args.id);
if (index === -1) {
return false;
}
users.splice(index, 1);
return true;
},
},
};
逻辑都是熟悉的数组操作:push 新增、Object.assign 合并更新、splice 删除。resolver 拿到 args.input 后怎么写数据,和写普通 Node.js 代码没有区别。
启动服务,在 Sandbox 里依次试验:
mutation {
updateUser(id: "1", input: { age: 21 }) {
username
age
}
}
mutation {
deleteUser(id: "2")
}
💡 一次 Mutation 执行多个操作
和 Query 一样,一次 Mutation 请求里可以写多个操作,用别名区分:
mutation {
a: createUser(input: { username: "张三" }) {
id
}
b: createUser(input: { username: "李四" }) {
id
}
}
这里有个重要的执行规则:同一层级的多个 Mutation 字段是串行执行的,一个一个来;而 Query 的多个字段是并行执行的。这样设计是为了保证写操作的顺序可预期,比如「先创建再更新」不会乱序。
🧾 小节总结
- Mutation 负责写操作,Query 负责只读查询,这是必须遵守的约定。
- Mutation 也要声明返回字段,通常返回修改后的数据,省去客户端二次查询。
- 用
input类型打包入参,配合变量传参,结构清晰易维护。 - 更新操作的 input 字段全部可空,只传要改的字段;删除可用
Boolean返回结果。 - 同一层级的多个 Mutation 串行执行,多个 Query 并行执行。
❓ 知识问答
Q1:为什么不直接用 REST 风格的增删改?
GraphQL 用统一的入口和语法覆盖读写,客户端不用记一堆 URL 和 HTTP 方法,而且返回值可以按需声明,这是它的设计取向。两种风格没有绝对优劣。
Q2:input 类型能嵌套 input 吗?
可以,比如 CreateArticleInput 里包含一个 CreateAuthorInput 字段。但建议保持扁平,嵌套太深会让调用方难以维护。
Q3:删除用户后,他的文章怎么办?
这是个业务设计问题:可以连带删除,也可以留着标记作者已注销。GraphQL 本身不管这些,逻辑都写在 resolver 里,按业务需要实现。
Q4:更新时想把某个字段改回「空」怎么办?
传 null 即可,比如 input: { city: null },Object.assign 会把它覆盖成 null。注意「不传这个字段」和「传 null」是两回事,前者不改动,后者会清空。
Q5:Mutation 里可以查询吗?
可以。resolver 是普通函数,里面既能写数据也能读数据,返回修改后的完整对象就是「写 + 读」的组合。
🧪 小练习
练习一:给文章实现 createArticle(input: CreateArticleInput!): Article!,input 包含 title 和 authorId,创建后返回文章的 id、标题。
# 请在这里编写代码
练习二:实现 updateArticle(id: ID!, input: UpdateArticleInput!): Article,要求找不到文章时返回 null,然后在 Sandbox 里验证两种情况。
const resolvers = {
Mutation: {
// 请在这里编写代码
},
};
🎉 恭喜你已经掌握 GraphQL 的增删改啦!目前服务还是独立运行的,下一篇我们把它集成进 Express,和已有的 REST 接口共存于同一个项目。
