服务端执行
Resolver、校验与执行引擎。
35 分钟
服务端执行
这一课你将能做到
- 完整讲出一个查询从 HTTP 请求到 JSON 响应的四个阶段(解析→校验→执行→序列化)
- 写出 Resolver 函数并用 DataLoader 消灭 N+1 查询
- 给服务加上查询深度/复杂度限制,挡住恶意查询
为什么要学
客户端写出优雅的查询只是上半场。下半场在服务端:一个 20 个课时的列表,如果每个课时单独查一次作者,就是 21 次 SQL——这就是 GraphQL 最著名的 N+1 陷阱。不理解执行引擎的工作方式,你的服务会在流量上来后死得很难看,而你甚至不知道是哪段查询干的。
人话:Resolver 是图的每条「边」的收费站,执行引擎按你声明的子树逐个过站收费;收费站效率决定整条路的体验。
先建立图像
一次查询的服务端旅程:
POST /graphql
│
▼
① Parse 文本 → AST(语法树),语法错误在此报 400
│
▼
② Validate AST 对 Schema 校验(字段存在?类型匹配?变量用到?)→ 400
│
▼
③ Execute 从根字段开始,逐字段调用 Resolver:
│ query ──lesson──▶ track ──lessons──▶ [author ×20]
│ 同层并行,父子串行
▼
④ Serialize 按可空性规则冒泡拼装 → { data, errors? } → 200
跟着做
步骤 1:写出 Resolver(以 graphql-java 风格伪码 + JS 双示)
Resolver 的四要素:父对象、参数、上下文、信息。
// JS (graphql-yoga) 写法
const resolvers = {
Query: {
lesson: (_parent, { slug }, { services }) =>
services.lesson.findBySlug(slug)
},
Lesson: {
// 父对象 lesson 已加载,这里只补 author
author: (lesson, _args, { loaders }) =>
loaders.user.load(lesson.authorId),
track: (lesson, _args, { loaders }) =>
lesson.trackId ? loaders.track.load(lesson.trackId) : null
}
}
// Java (Spring for GraphQL) 等价写法
@QueryMapping
public Lesson lesson(@Argument String slug) {
return lessonService.findBySlug(slug);
}
@SchemaMapping(typeName = "Lesson", field = "author")
public CompletableFuture<User> author(Lesson lesson, DataLoader<String, User> loader) {
return loader.load(lesson.getAuthorId());
}
步骤 2:亲手制造一次 N+1
查询:
{ lessons(first: 20) { title author { name } } }
没有批处理时的 SQL 日志:
SELECT * FROM lesson LIMIT 20; -- 1 次
SELECT * FROM user WHERE id = 'u1'; -- 第 1 课时的作者
SELECT * FROM user WHERE id = 'u7'; -- 第 2 课时的作者
... -- × 20
21 次查询。列表变 100 就是 101 次。图越深越致命。
步骤 3:DataLoader 批处理 + 请求级缓存
核心思想:把同一 tick 内的多次 load 合并成一次 batch 查询。
function createUserLoader(db) {
return new DataLoader(async (ids: string[]) => {
const users = await db.user.findMany({ where: { id: { in: ids } } })
const byId = new Map(users.map(u => [u.id, u]))
return ids.map(id => byId.get(id) ?? null) // 必须按入参顺序返回、长度一致
})
}
// 每个请求新建一批 loader(缓存作用域 = 单次请求,防串数据)