阶段三:GraphQL
Query和Mutation是GraphQL与数据交互的两把钥匙。Query负责读取数据,支持别名、片段、变量等强大特性;Mutation负责写入数据,保证顺序执行。本课深入查询的高级特性和变更的最佳实践。
# 变量让查询可复用、可缓存、更安全
# 查询定义
query GetUsers($limit: Int = 20, $role: Role, $cursor: String) {
users(limit: $limit, role: $role, after: $cursor) {
edges {
node { id name email role }
cursor
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
# 变量值
{
"limit": 10,
"role": "EDITOR",
"cursor": null
}
# 使用curl发送带变量的查询
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query GetUsers($limit: Int, $role: Role) { users(limit: $limit, role: $role) { edges { node { id name } } } }",
"variables": { "limit": 5, "role": "EDITOR" }
}'
# 别名:同一查询中多次请求同一字段,用不同参数
query CompareUsers {
admin: user(id: "1") {
name
role
}
editor: user(id: "2") {
name
role
}
# 不用别名会冲突!
}
# 响应
{
"data": {
"admin": { "name": "张三", "role": "ADMIN" },
"editor": { "name": "李四", "role": "EDITOR" }
}
}
# 片段:可复用的字段组合,DRY原则
fragment UserFields on User {
id
name
email
avatar
role
}
fragment PostFields on Post {
id
title
excerpt
createdAt
author { ...UserFields }
}
# 在查询中使用片段
query GetPostsWithAuthors {
posts(first: 10) {
edges {
node {
...PostFields
comments(first: 3) {
edges {
node {
body
author { ...UserFields }
}
}
}
}
}
}
}
# 内联片段:处理接口和联合类型
query Search {
search(query: "GraphQL") {
... on User { id name email }
... on Post { id title author { name } }
... on Tag { id name slug }
}
}
# @include 和 @skip:条件查询
query GetUsers($withPosts: Boolean!, $role: Role) {
users(role: $role) {
edges {
node {
id
name
posts @include(if: $withPosts) {
edges { node { id title } }
}
postCount @include(if: $withPosts)
}
}
}
}
# 变量
{ "withPosts": true, "role": "ADMIN" }
# @skip:条件跳过
query GetPosts($skipContent: Boolean!) {
posts {
edges {
node {
id
title
content @skip(if: $skipContent) # 列表页跳过内容
excerpt
}
}
}
}
# @deprecated:标记废弃字段
type User {
name: String! @deprecated(reason: "使用 displayName 代替")
displayName: String!
}
# 模式1: 输入类型模式(推荐)
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(input: UpdatePostInput!): UpdatePostPayload!
deletePost(input: DeletePostInput!): DeletePostPayload!
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]!
status: PostStatus = DRAFT
clientMutationId: String # Relay规范:幂等性保障
}
type CreatePostPayload {
post: Post # 新创建的资源
user: User # 可能影响的其他资源
clientMutationId: String
}
# 模式2: 简单模式(小型API)
type Mutation {
createPost(title: String!, content: String!): Post!
updatePost(id: ID!, title: String, content: String): Post!
deletePost(id: ID!): Boolean!
}
mutation { a b c }中a先于b,b先于c。但最佳实践是一个Mutation只包含一个操作,确保行为可预测。
# 成功响应
mutation {
createPost(input: { title: "测试", content: "内容" }) {
post { id title status createdAt }
clientMutationId
}
}
# → { "data": { "createPost": { "post": {...}, "clientMutationId": null } } }
# 验证错误
mutation {
createPost(input: { title: "", content: "" }) {
post { id }
}
}
# → {
# "data": { "createPost": null },
# "errors": [{
# "message": "验证失败",
# "extensions": {
# "code": "VALIDATION_ERROR",
# "details": [
# { "field": "title", "message": "标题不能为空" },
# { "field": "content", "message": "内容不能为空" }
# ]
# }
# }]
# }
// query-mutation-server.js
const express = require('express');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema } = require('graphql');
const app = express();
const schema = buildSchema(`
scalar DateTime
enum TaskStatus { TODO IN_PROGRESS DONE CANCELLED }
enum Priority { LOW MEDIUM HIGH URGENT }
type User {
id: ID!
name: String!
email: String!
tasks(status: TaskStatus, first: Int = 20): [Task!]!
taskCount(status: TaskStatus): Int!
}
type Task {
id: ID!
title: String!
description: String
status: TaskStatus!
priority: Priority!
assignee: User
tags: [String!]!
dueDate: DateTime
createdAt: DateTime!
updatedAt: DateTime!
}
type TaskConnection {
edges: [TaskEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type TaskEdge {
node: Task!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input CreateTaskInput {
title: String!
description: String
priority: Priority = MEDIUM
assigneeId: ID
tags: [String!]! = []
dueDate: DateTime
}
input UpdateTaskInput {
title: String
description: String
status: TaskStatus
priority: Priority
assigneeId: ID
tags: [String!]
dueDate: DateTime
}
type Query {
me: User!
user(id: ID!): User
users(first: Int = 20): [User!]!
task(id: ID!): Task
tasks(status: TaskStatus, priority: Priority, assigneeId: ID, tag: String, first: Int = 20, after: String): TaskConnection!
searchTasks(query: String!): [Task!]!
}
type Mutation {
createTask(input: CreateTaskInput!): Task!
updateTask(id: ID!, input: UpdateTaskInput!): Task!
deleteTask(id: ID!): Boolean!
assignTask(taskId: ID!, userId: ID!): Task!
changeStatus(taskId: ID!, status: TaskStatus!): Task!
}
`);
// 数据
const users = [
{ id: '1', name: '张三', email: 'zhang@example.com' },
{ id: '2', name: '李四', email: 'li@example.com' },
];
const tasks = [
{ id: '1', title: '设计API', description: '完成RESTful API设计', status: 'IN_PROGRESS', priority: 'HIGH', assigneeId: '1', tags: ['API', '设计'], dueDate: '2025-02-01', createdAt: '2025-01-10', updatedAt: '2025-01-15' },
{ id: '2', title: '编写文档', description: 'API使用文档', status: 'TODO', priority: 'MEDIUM', assigneeId: '2', tags: ['文档'], dueDate: '2025-02-15', createdAt: '2025-01-11', updatedAt: '2025-01-11' },
{ id: '3', title: '性能优化', description: 'API响应时间优化', status: 'TODO', priority: 'URGENT', assigneeId: '1', tags: ['API', '性能'], dueDate: '2025-01-25', createdAt: '2025-01-12', updatedAt: '2025-01-12' },
{ id: '4', title: '添加测试', description: '单元测试和集成测试', status: 'DONE', priority: 'MEDIUM', assigneeId: '2', tags: ['测试'], dueDate: null, createdAt: '2025-01-08', updatedAt: '2025-01-14' },
];
let nextTaskId = 5;
const root = {
me: () => users[0],
user: ({ id }) => users.find(u => u.id === id),
users: ({ first }) => users.slice(0, first),
task: ({ id }) => tasks.find(t => t.id === id),
tasks: ({ status, priority, assigneeId, tag, first, after }) => {
let result = [...tasks];
if (status) result = result.filter(t => t.status === status);
if (priority) result = result.filter(t => t.priority === priority);
if (assigneeId) result = result.filter(t => t.assigneeId === assigneeId);
if (tag) result = result.filter(t => t.tags.includes(tag));
const startIdx = after ? result.findIndex(t => t.id === after) + 1 : 0;
const sliced = result.slice(startIdx, startIdx + first);
return {
edges: sliced.map(t => ({ node: t, cursor: t.id })),
pageInfo: { hasNextPage: startIdx + first < result.length, hasPreviousPage: startIdx > 0, endCursor: sliced[sliced.length - 1]?.id },
totalCount: result.length,
};
},
searchTasks: ({ query }) => {
const q = query.toLowerCase();
return tasks.filter(t => t.title.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q));
},
User: {
tasks: (user, { status }) => {
let result = tasks.filter(t => t.assigneeId === user.id);
if (status) result = result.filter(t => t.status === status);
return result;
},
taskCount: (user, { status }) => {
let result = tasks.filter(t => t.assigneeId === user.id);
if (status) result = result.filter(t => t.status === status);
return result.length;
},
},
Task: {
assignee: (task) => task.assigneeId ? users.find(u => u.id === task.assigneeId) : null,
},
// Mutations
createTask: ({ input }) => {
const id = String(nextTaskId++);
const task = { id, ...input, status: 'TODO', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
tasks.push(task);
return task;
},
updateTask: ({ id, input }) => {
const task = tasks.find(t => t.id === id);
if (!task) throw new Error(`任务 ${id} 不存在`);
Object.assign(task, input, { updatedAt: new Date().toISOString() });
return task;
},
deleteTask: ({ id }) => {
const idx = tasks.findIndex(t => t.id === id);
if (idx === -1) return false;
tasks.splice(idx, 1);
return true;
},
assignTask: ({ taskId, userId }) => {
const task = tasks.find(t => t.id === taskId);
if (!task) throw new Error(`任务 ${taskId} 不存在`);
if (!users.find(u => u.id === userId)) throw new Error(`用户 ${userId} 不存在`);
task.assigneeId = userId;
task.updatedAt = new Date().toISOString();
return task;
},
changeStatus: ({ taskId, status }) => {
const task = tasks.find(t => t.id === taskId);
if (!task) throw new Error(`任务 ${taskId} 不存在`);
task.status = status;
task.updatedAt = new Date().toISOString();
return task;
},
};
app.all('/graphql', createHandler({ schema, rootValue: root }));
app.listen(3000, () => console.log('📋 查询变更服务运行在 http://localhost:3000/graphql'));
# 1. 带变量的查询
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query($status: TaskStatus, $first: Int) { tasks(status: $status, first: $first) { edges { node { id title priority } } totalCount } }",
"variables": { "status": "TODO", "first": 5 }
}' | jq .
{
"data": {
"tasks": {
"edges": [
{"node":{"id":"2","title":"编写文档","priority":"MEDIUM"}},
{"node":{"id":"3","title":"性能优化","priority":"URGENT"}}
],
"totalCount": 2
}
}
}
# 2. 别名查询
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ todo: tasks(status: TODO) { totalCount } done: tasks(status: DONE) { totalCount } all: tasks { totalCount } }"}' | jq .
# 3. 片段查询
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"fragment TaskFields on Task { id title status priority } { tasks(first: 3) { edges { node { ...TaskFields assignee { name } } } } }"}' | jq .
# 4. 条件查询(@include)
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query($withDesc: Boolean!) { tasks(first: 3) { edges { node { id title description @include(if: $withDesc) } } } }", "variables": {"withDesc": false}}' | jq .
# 5. Mutation: 创建任务
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createTask(input: { title: \"Code Review\", priority: HIGH, tags: [\"review\"] }) { id title status priority } }"}' | jq .
# 6. Mutation: 更改状态
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { changeStatus(taskId: \"2\", status: IN_PROGRESS) { id title status } }"}' | jq .
将以下冗余查询用片段重构:
{ user1: user(id:"1") { id name email avatar role } user2: user(id:"2") { id name email avatar role } }
设计一个批量更新任务状态的Mutation,支持一次更新多个任务。