N+1 与抓取策略
fetch join、@EntityGraph。
30 分钟
N+1 与抓取策略
加载 10 门课程再逐个查课时,JPA 可能发出 1 + 10 条 SQL——这就是 N+1 问题。懂抓取策略才能写不出「本地快、上线慢」的接口。
学习目标
- 识别 N+1 的触发条件
- 使用
JOIN FETCH、@EntityGraph - 对照 MyBatis 一次 JOIN 查询
问题复现
List<Track> tracks = trackRepository.findAll(); // 1 条 SQL
for (Track t : tracks) {
t.getLessons().size(); // 每条 track 再 1 条 → N+1
}
日志里会看到:SELECT ... FROM cf_track,然后多次 SELECT ... FROM cf_lesson WHERE track_id = ?。
解法 1:JOIN FETCH
@Query("SELECT DISTINCT t FROM Track t LEFT JOIN FETCH t.lessons")
List<Track> findAllWithLessons();
一次 JOIN 拉回 Track + Lesson。注意 DISTINCT 防重复行。
解法 2:@EntityGraph
@EntityGraph(attributePaths = {"lessons"})
List<Track> findByCategory(String category);
声明本次查询需要预加载 lessons 关联。
抓取类型
| 类型 | 行为 | 默认 |
|---|---|---|
| LAZY | 访问时才查 | @OneToMany |
| EAGER | 主查询后立即查 | 慎用 |
原则:关联默认 LAZY;需要时在查询层显式 fetch,避免全局 EAGER 拖垮列表接口。
MyBatis 侧
MyBatis 无「隐式懒加载集合」——N+1 通常是你写了循环查 Mapper。复杂场景用一条 JOIN + resultMap 嵌套:
<select id="selectTrackWithLessons" resultMap="trackWithLessonsMap">
SELECT t.*, l.id AS lid, l.title AS ltitle
FROM cf_track t
LEFT JOIN cf_lesson l ON l.track_id = t.id
WHERE t.slug = #{slug}
</select>
相关课程
练习
- 打开 SQL 日志(
spring.jpa.show-sql),复现 N+1 - 用
JOIN FETCH改写 并对比 SQL 条数