N+1 与 DataLoader
批量加载、性能直觉。
30 分钟
N+1 与 DataLoader
GraphQL 字段级 Resolver 易导致 N+1 查询:列表含 N 条 track,每条解析 lessons 时各打一次 SQL。REST 列表接口常用 JOIN 一次取回;GraphQL 需 批量加载 意识。
学习目标
- 识别 N+1 场景与日志特征
- 理解 DataLoader 批处理与缓存语义
- 知道何时 REST 一次 JOIN 更简单
核心内容
问题演示
Query:
query {
tracks { slug lessons { title } }
}
朴素实现:
@SchemaMapping
public List<Lesson> lessons(Track track) {
// 每个 track 一次 SELECT → 1 + N 次查询
return lessonRepo.findByTrackSlug(track.getSlug());
}
假设 20 个 track → 21 次数据库往返。MySQL 主库 cf_lesson 在 spring-boot-essentials JPA 层同样受影响。
DataLoader 思路
@Bean
public BatchLoader<String, List<Lesson>> lessonsByTrackSlugLoader(LessonRepository repo) {
return slugs -> CompletableFuture.supplyAsync(() -> {
Map<String, List<Lesson>> grouped = repo.findByTrackSlugIn(slugs)
.stream()
.collect(Collectors.groupingBy(Lesson::getTrackSlug));
return slugs.stream().map(s -> grouped.getOrDefault(s, List.of())).toList();
});
}
同一请求 tick 内,多个 lessons Resolver 注册 key,框架 合并为一次 WHERE track_slug IN (...)。
REST 对照
GET /tracks?include=lessons 可在 Service 层一次 JOIN 或两次查询批量填充——CodeRoadMap 当前 REST 未内嵌 lessons,课时走独立 URL,换用 GraphQL 时必须主动优化,否则性能反劣于简单 REST。
MongoDB 场景
mongodb-essentials 嵌套文档可减少 JOIN,但 $lookup 聚合仍有成本;DataLoader 对 跨 collection 批量同样适用。
练习
- 开启 SQL 日志,执行含 10+ track 的嵌套 Query,数 SELECT 次数
- 设计
findByTrackSlugInRepository 方法签名 - 写一句话:为何 GraphQL「灵活」不等于「更快」
小结
N+1 是 GraphQL 必修课;DataLoader 批处理是标准解。 下一课 Apollo Client 前端查询与缓存。
加载本节练习…