大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
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改写findByCategory并对比 SQL 条数 - 分页列表页为何不宜
JOIN FETCH大集合(提示:内存与 DISTINCT)
小结
N+1 是 ORM 懒加载的代价;用 fetch 策略或 JOIN 一次取齐。 下一课讲 MariaDB 与 MySQL 兼容性。