Skip to content

GraphQL - Mutation

虽然 Query(查询操作)用于获取数据,但 Mutation(变更操作)用于修改服务器端数据。这包括创建新数据、更新现有数据或删除数据等操作。可以将 mutation 理解为 GraphQL 中对应 REST 请求的 POST、PUT、PATCH 或 DELETE。

Mutation 在 GraphQL 模式(Schema)中的根 Mutation 类型下被定义为字段(Field)。与 Query 类似,它们可以接受参数(通常分组在 input 对象类型中),并返回特定数据,通常是刚刚被修改的数据。

Mutation 请求看起来与 Query 非常相似,但它必须以 mutation 关键字开头。

# 基本 Mutation 语法
mutation OptionalOperationName {
mutationFieldName(argumentName: "value", ...) {
# Fields to return after the mutation succeeds
field1
field2
...
}
}

与 Query 的主要区别:

  • 关键字: 必须使用 mutation 关键字。
  • 执行: 虽然服务器通常可以并行执行 Query 字段,但顶层 Mutation 字段通常是串行执行的(一个接一个),以确保可预测的副作用(Side effects)并避免竞态条件(Race conditions)。
  • 返回值: Mutation 返回反映状态变化的数据至关重要。这使得客户端(如 Apollo Client)能够准确地更新其缓存(Cache)。

为每个 mutation 的参数定义一个单独的、专用的 input 对象类型是一个强烈建议的约定和最佳实践。这使得 mutation 更容易演进(稍后添加可选字段)并保持参数列表的整洁。

# 带有 Input 类型的模式定义
# 用于创建学生的 Input 对象类型
input CreateStudentInput {
firstName: String!
lastName: String!
collegeId: ID! # Assuming college must exist
email: String # Optional field
}
# 定义 Mutation 返回的 Student 类型
type Student {
id: ID!
firstName: String!
lastName: String!
email: String
collegeId: ID!
}
# 使用 Input 类型定义 Mutation
type Mutation {
createStudent(input: CreateStudentInput!): Student # 返回创建的学生对象
}

让我们实现上面定义的 createStudent mutation。

假设一个基本的 Apollo Server 设置和一个带有添加学生功能的模拟数据源 (db.js)。

// db.js(模拟数据 - 带有创建函数的示例)
let students = [
{ id: 'S1001', firstName: 'Mohtashim', lastName: 'Mohammad', collegeId: 'col-102' },
{ id: 'S1002', firstName: 'Kannan', lastName: 'Sudhakaran', collegeId: 'col-101' },
];
let nextStudentId = 1003; // 模拟用的简单 ID 生成
export const db = {
students: {
list: () => students,
get: (id) => students.find(s => s.id === id),
create: (input) => {
const newStudent = {
id: `S${nextStudentId++}`,
...input // 展开 input 对象中的属性
};
students.push(newStudent);
console.log("Current students:", students); // 记录当前状态
return newStudent; // 返回新创建的学生对象
}
},
// 假设 Colleges 数据存在,用于需要时进行验证
colleges: {
get: (id) => { /* ... 查找 College 的逻辑 ... */ return { id: id, name: 'Some College'}; },
}
};

步骤 2:定义模式(schema.graphql 或在 server.js 中)

Section titled “步骤 2:定义模式(schema.graphql 或在 server.js 中)”

使用之前定义的模式,包括 CreateStudentInput、Student 和 Mutation 类型。

#graphql
type Query { # 有效模式至少需要一个 Query 字段
_: Boolean
}
input CreateStudentInput {
firstName: String!
lastName: String!
collegeId: ID!
email: String
}
type Student {
id: ID!
firstName: String!
lastName: String!
email: String
collegeId: ID!
}
type Mutation {
createStudent(input: CreateStudentInput!): Student
}

步骤 3:为 Mutation 创建解析器(Resolver)

Section titled “步骤 3:为 Mutation 创建解析器(Resolver)”

在 Mutation 类型解析器映射中,实现 createStudent 字段的解析器函数。

// resolvers.js
import { db } from './db.js';
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
_: () => true, // 占位符 Query 的解析器
},
Mutation: {
createStudent: (parent, args, context, info) => {
// input 对象可在 args.input 中获取
const { input } = args;
console.log('Resolving: Mutation.createStudent with input:', input); // 正在解析:Mutation.createStudent,输入为:
// --- 可选验证 ---
// 示例:检查 collegeId 是否存在(在实际应用中)
const collegeExists = db.colleges.get(input.collegeId);
if (!collegeExists) {
throw new GraphQLError(`College with ID ${input.collegeId} not found.`, {
extensions: { code: 'BAD_USER_INPUT' }
});
}
// 根据需要添加其他验证(邮箱格式等)
// --- 验证结束 ---
// 调用数据源函数创建学生
try {
const newStudent = db.students.create(input);
// 返回新创建的学生对象
// 这与模式中定义的 'Student' 返回类型相匹配
return newStudent;
} catch (error) {
// 处理创建过程中可能出现的错误
console.error("Error creating student:", error);
throw new GraphQLError('Failed to create student.', {
extensions: { code: 'INTERNAL_SERVER_ERROR' }
});
}
},
},
// 可选:如果需要,添加 Student 解析器(例如,用于解析 College 对象)
// Student: {
// college: (parent) => db.colleges.get(parent.collegeId)
// }
};
export default resolvers;

要点:

  • 解析器通过 args.input 访问参数。
  • 它执行可选的验证(检查 collegeId 是否存在)。
  • 它调用适当的数据源方法(db.students.create)来执行写入操作。
  • 至关重要的是,它返回新创建的学生对象。这使得客户端能够获取服务器生成的 ID 并更新其状态/缓存。

将模式和解析器集成到你的 Apollo Server 设置中并运行服务器(npm start)。

使用 Apollo Sandbox(http://localhost:9000/graphql)执行 mutation。记住使用 mutation 关键字。

# 使用变量的 Mutation 请求
mutation AddNewStudent($studentData: CreateStudentInput!) {
createStudent(input: $studentData) {
# Specify which fields of the created student you want back
id
firstName
lastName
email
collegeId
}
}

在 ‘Variables’ 面板中,提供输入数据:

{
"studentData": {
"firstName": "Tim",
"lastName": "George",
"collegeId": "col-101",
"email": "tim.g@example.com"
}
}

执行 mutation。响应应包含新创建学生的数据,包括其服务器生成的 ID:

{
"data": {
"createStudent": {
"id": "S1003", // 示例生成的 ID
"firstName": "Tim",
"lastName": "George",
"email": "tim.g@example.com",
"collegeId": "col-101"
}
}
}

之后你可以通过运行 students 或 studentById 查询来验证创建是否成功,或者检查你的模拟数据控制台日志。

因为 mutation 可以返回模式中定义的任何类型,你可以设计它们同时返回相关数据,从而节省客户端额外的获取操作。

示例:如果你希望 createStudent mutation 返回学生以及其关联的学院详细信息:

# 1. 修改模式:添加 College 类型并关联
type College {
id: ID!
name: String!
}
type Student {
id: ID!
# ... 其他字段
college: College # 添加关联字段
}
type Mutation {
createStudent(input: CreateStudentInput!): Student # 返回类型仍然是 Student
}
# 2. 添加 Student.college 的解析器(如 Query 章节所示)
# 在 resolvers.js 中
# Student: {
# college: (parent) => db.colleges.get(parent.collegeId)
# }
# 3. 修改 Mutation 请求以请求嵌套数据
mutation AddStudentAndGetCollege($studentData: CreateStudentInput!) {
createStudent(input: $studentData) {
id
firstName
college { # 请求嵌套的 college 数据
id
name
}
}
}

createStudent mutation 解析器本身没有显著变化(它仍然只是创建并返回学生对象)。当客户端在 mutation 的返回选择集中请求嵌套的 college 字段时,Student.college 解析器会自动为新创建的学生对象运行,然后在最终响应发送之前将 College 数据填充进去。