Mutation 基础
Schema 定义
input CreatePostInput {
title: String!
content: String!
tags: [String!]
published: Boolean = false
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
published: Boolean
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): DeletePostResult!
publishPost(id: ID!): Post!
}
客户端调用
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
published
createdAt
author { name }
}
}
# Variables:
# {
# "input": {
# "title": "Hello GraphQL",
# "content": "...",
# "tags": ["graphql", "api"]
# }
# }
Resolver 实现
const resolvers = {
Mutation: {
createPost: async (
_: unknown,
{ input }: { input: CreatePostInput },
{ db, user }: Context
) => {
// 验证用户已登录
if (!user) throw new AuthenticationError('Must be logged in');
// 创建帖子
const post = await db.posts.create({
data: {
...input,
authorId: user.id,
},
});
return post;
},
deletePost: async (_, { id }, { db, user }) => {
const post = await db.posts.findUnique({ where: { id } });
if (!post) throw new UserInputError('Post not found');
if (post.authorId !== user.id) throw new ForbiddenError('Not your post');
await db.posts.delete({ where: { id } });
return { success: true, id };
},
},
};
错误处理模式
方案一:GraphQL errors 数组(Apollo 默认)
{
"data": { "createPost": null },
"errors": [
{
"message": "Title cannot be empty",
"locations": [{ "line": 2, "column": 3 }],
"path": ["createPost"],
"extensions": {
"code": "BAD_USER_INPUT",
"field": "title"
}
}
]
}
方案二:Union 错误类型(推荐)
type ValidationError {
field: String!
message: String!
}
type CreatePostSuccess {
post: Post!
}
type CreatePostError {
message: String!
validationErrors: [ValidationError!]
}
# Mutation 返回 Union 类型
union CreatePostResult = CreatePostSuccess | CreatePostError
type Mutation {
createPost(input: CreatePostInput!): CreatePostResult!
}
# 客户端查询
mutation {
createPost(input: { title: "", content: "..." }) {
__typename
... on CreatePostSuccess {
post { id title }
}
... on CreatePostError {
message
validationErrors { field message }
}
}
}
文件上传(multipart)
scalar Upload
type Mutation {
uploadAvatar(file: Upload!, userId: ID!): User!
uploadMultiple(files: [Upload!]!): [File!]!
}
import { GraphQLUpload, FileUpload } from 'graphql-upload';
const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadAvatar: async (_, { file, userId }, { db, storage }) => {
const { createReadStream, filename, mimetype }: FileUpload = await file;
// 上传到对象存储(S3/OSS)
const url = await storage.upload({
stream: createReadStream(),
key: `avatars/${userId}/${filename}`,
contentType: mimetype,
});
return db.users.update({
where: { id: userId },
data: { avatarUrl: url },
});
},
},
};
Mutation 中的串行执行
GraphQL 查询中多个字段并行执行,但 Mutation 中多个操作保证串行执行,按从上到下的顺序依次运行。这对于依赖前一个操作结果的场景非常重要。
input 类型设计最佳实践
为何使用 input 类型而非直接参数
将多个参数包装为 input 类型有三个优势:1) 可扩展性——新增字段不改变 Mutation 签名;2) 客户端可以将 input 对象声明为变量复用;3) 类型文档清晰,与普通 Object 类型区分(input 类型不能有 Resolver,只能包含标量/enum/其他 input 类型)。
Create vs Update input 分离
创建时必填字段(如 title!)在更新时应为可选(title: String),所以通常 CreateXxxInput 和 UpdateXxxInput 是两个不同类型。不要复用同一个 input 类型同时用于创建和更新。
返回值设计
Mutation 应返回被修改的对象(而非 Boolean),让客户端缓存能自动更新相关字段。deleteXxx 可返回被删除对象的 ID 或
{ success: Boolean!, deletedId: ID! } 结构,便于客户端从缓存中移除。两种错误处理方案对比
errors 数组方案(Apollo 默认)
Resolver 抛出 GraphQLError,错误出现在响应顶层 errors 数组中,data.createPost 为 null。优点:简单;缺点:客户端需要额外处理 errors 数组,难以在 TypeScript 中类型安全地访问错误详情。
Union 错误类型方案(推荐)
Mutation 返回
CreatePostResult = CreatePostSuccess | CreatePostError Union 类型,错误作为数据字段返回(data.createPost.__typename === 'CreatePostError')。优点:类型安全、可携带结构化验证错误、客户端用 inline fragment 处理;缺点:Schema 更复杂。
事务性 Mutation
// Prisma 事务:多个写操作原子性执行
const resolvers = {
Mutation: {
transferCredits: async (_, { fromId, toId, amount }, { db }) => {
// $transaction 确保两个操作要么全成功,要么全失败
const [from, to] = await db.$transaction([
// 扣减发送方积分
db.users.update({
where: { id: fromId },
data: { credits: { decrement: amount } },
}),
// 增加接收方积分
db.users.update({
where: { id: toId },
data: { credits: { increment: amount } },
}),
]);
// 检查发送方余额是否足够(事务后验证)
if (from.credits < 0) {
throw new GraphQLError('Insufficient credits', {
extensions: { code: 'BAD_USER_INPUT' }
});
}
return { from, to, amount };
},
},
};
本章小结
本章核心要点
- input 类型设计:Create 和 Update 用不同 input 类型(必填 vs 可选);Mutation 应返回被修改的对象而非 Boolean,便于客户端缓存自动更新。
- 两种错误处理:throws GraphQLError(简单,errors 数组)vs Union 类型(类型安全,推荐);生产代码推荐 Union 方案,让错误成为可预期的数据而非意外。
- Mutation 串行执行:同一请求中多个 Mutation 字段按顺序串行执行(不同于 Query 的并行),这是 GraphQL 规范保证的行为。
- 文件上传:使用 graphql-upload 包定义 Upload scalar,通过 multipart/form-data 协议传输;Resolver 中解构 createReadStream/filename/mimetype 处理上传流。
- 事务处理:多个写操作需要原子性时,用 Prisma $transaction 或原生 DB 事务包裹;事务完成后再抛出业务错误(如余额不足),确保数据一致性。
- 安全验证顺序:Mutation Resolver 中应先检查认证(requireAuth)→ 检查授权(requireRole/isOwner)→ 验证输入(zod/manual)→ 执行业务逻辑,不要省略任何一步。