Mybatis 是否支持延迟加载?实现原理是什么?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
概念理解:面试官不仅仅是想知道 "支不支持",更是想考察你是否理解延迟加载的触发时机——什么时候该加载,什么时候不该加载,以及它和立即加载的区别。
-
底层原理:看你能否说清楚 MyBatis 延迟加载的底层实现——通过动态代理拦截属性访问,在真正需要数据时才执行 SQL。
-
实践意识:考察你是否知道延迟加载的适用场景、配置方式,以及它可能带来的 N+1 查询问题。
核心答案
MyBatis 支持延迟加载。延迟加载(Lazy Loading)是指在查询主对象时,不立即加载关联的子对象,只有在真正访问子对象属性时,才触发 SQL 去查询。
| 维度 | 说明 |
|---|---|
| 适用场景 | <association> 一对一、<collection> 一对多关联查询 |
| 配置方式 | fetchType = "lazy" 或全局配置 lazyLoadingEnabled = true |
| 底层原理 | 动态代理(CGLIB / Javassist)拦截属性访问,触发时才执行 SQL |
| 前提条件 | 必须使用 resultMap + 子查询(select 属性),不能是 JOIN 查询 |
一句话结论:延迟加载通过动态代理实现,关联对象在真正被访问时才触发 SQL 查询,减少不必要的数据库访问。
深度解析
一、延迟加载 vs 立即加载
两种加载策略的核心区别在于关联对象的查询时机:
- 立即加载:查询主对象时,一并执行关联对象的 SQL。不管你后面用不用得到关联数据,SQL 都执行了。适合关联数据大概率会用的场景。
- 延迟加载:查询主对象时,只返回主对象数据,关联对象用一个代理对象占位。当你真正调用
user.getDept()访问关联属性时,代理对象才触发 SQL 查询。适合关联数据不一定会用到的场景。
二、配置方式
方式一:全局配置
在 mybatis-config.xml 中开启全局延迟加载:
<settings>
<!-- 开启延迟加载(默认 false) -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 按需加载(默认 true,3.4.1 及以后版本)
true:按需加载,访问任意延迟属性时只加载该属性
false:访问任意属性时触发加载所有延迟属性 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
方式二:局部配置(推荐)
在 resultMap 的 <association> 或 <collection> 中单独指定,优先级高于全局配置:
<resultMap id="userWithDeptMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<!-- fetchType = "lazy" 表示延迟加载 -->
<association property="dept" column="dept_id"
select="com.example.mapper.DeptMapper.selectById"
fetchType="lazy"/>
<!-- fetchType = "eager" 表示立即加载(默认) -->
<collection property="articles" column="id"
select="com.example.mapper.ArticleMapper.selectByUserId"
fetchType="eager"/>
</resultMap>
<!-- 主查询:只查用户 -->
<select id="selectById" resultMap="userWithDeptMap">
SELECT id, username, dept_id FROM t_user WHERE id = #{id}
</select>
关键点:延迟加载必须使用 resultMap + select 子查询的方式,不能用 JOIN 查询。因为 JOIN 查询已经在一条 SQL 中把所有数据都查出来了,没有 "延迟" 的空间。
三、底层实现原理
MyBatis 延迟加载的底层通过动态代理实现,具体流程如下:
延迟加载的实现分为以下关键步骤:
- 步骤一(主查询执行):执行主对象的 SQL 查询,获取主对象数据(如 User 的
id、username、dept_id)。 - 步骤二(代理对象创建):MyBatis 检测到
<association>配置了fetchType="lazy",不执行子查询 SQL,而是通过ProxyFactory创建一个 Dept 接口的代理对象。代理对象内部持有一个ResultLoader(结果加载器),记录了要执行的 SQL 和参数。 - 步骤三(延迟触发):当开发者调用
user.getDept().getName()时,代理对象拦截到方法调用。判断这是一个属性访问(getter 方法),触发ResultLoader.load(),执行子查询 SQL,获取真实的 Dept 数据,并用真实对象替换代理对象。 - 步骤四(结果替换):代理对象将自身的所有方法调用委托给真实对象,后续再次访问 Dept 属性时直接返回缓存数据,不会重复查询。
四、动态代理的实现细节
MyBatis 默认使用 Javassist 生成代理类(MyBatis 3.3 之前),3.3 之后支持切换为 CGLIB:
<!-- 配置代理工具(默认 JAVASSIST) -->
<setting name="proxyFactory" value="CGLIB"/>
<!-- 或 -->
<setting name="proxyFactory" value="JAVASSIST"/>
| 代理方式 | 说明 |
|---|---|
| JAVASSIST(默认) | 通过字节码增强生成代理类,不需要接口 |
| CGLIB | 通过生成子类实现代理,同样不需要接口 |
代理类的核心逻辑:重写所有 getter 方法,在 getter 被调用时判断关联数据是否已加载,未加载则触发 SQL 查询。
五、N+1 问题——延迟加载的双刃剑
延迟加载虽然可以减少不必要的查询,但在某些场景下会导致 N+1 查询问题:
// 查询 100 个用户
List<User> users = userMapper.selectAll(); // 1 条 SQL
// 遍历访问每个用户的部门
for (User user : users) {
System.out.println(user.getDept().getName()); // 每次循环触发 1 条 SQL!
}
// 总共执行了 1 + 100 = 101 条 SQL 💥
解决方案:
| 方案 | 说明 |
|---|---|
| 改用 JOIN 查询 | 如果关联数据大概率会用到,直接用 JOIN 一次查完 |
| 批量加载 | MyBatis 3.5.9+ 支持 lazyLoadTriggerMethods 配置批量触发 |
| 控制访问粒度 | 在业务代码中避免在循环中访问延迟加载属性 |
六、常见误区
- 误区一:"延迟加载在任何场景下都能用"
- 不是。延迟加载必须使用
resultMap+select子查询的方式。如果你用 JOIN 查询把数据一次性查出来了,延迟加载就没有意义了。
- 不是。延迟加载必须使用
- 误区二:"延迟加载一定比立即加载好"
- 不是。如果关联数据大概率会被访问到,延迟加载反而会增加 SQL 次数(主查询 + 子查询 vs 一条 JOIN)。延迟加载适合 "关联数据不一定会用" 的场景。
- 误区三:"开启了全局延迟加载,所有查询都是延迟的"
- 不是。只有在
resultMap中使用select子查询配置的关联映射才会延迟。普通字段映射和resultType不受影响。
- 不是。只有在
面试高频追问
- 追问一:
aggressiveLazyLoading配置有什么用?- 当
aggressiveLazyLoading = true时,访问主对象的任意属性(不仅仅是延迟属性)都会触发所有延迟属性的加载。MyBatis 3.4.1 之后默认为false(按需加载),只有访问延迟属性本身时才触发加载。
- 当
- 追问二:延迟加载和 MyBatis 二级缓存能一起用吗?
- 可以,但需要注意:延迟加载触发的子查询也会走缓存。如果主对象在二级缓存中,关联对象的延迟加载可能读到过期的缓存数据。复杂场景下建议关闭二级缓存。
- 追问三:Spring 整合 MyBatis 后,延迟加载会不会失效?
- 可能会。如果 Controller 层直接访问延迟属性,但
SqlSession已经关闭了,就会报错。解决方案:使用OpenSessionInViewFilter(Spring MVC)或OpenSessionInViewInterceptor(Spring Boot),让SqlSession在整个请求期间保持打开。
- 可能会。如果 Controller 层直接访问延迟属性,但
常见面试变体
- 变体一:"MyBatis 的延迟加载怎么配置?"
- 变体二:"MyBatis 延迟加载的底层是怎么实现的?用了什么设计模式?"
- 变体三:"什么是 N+1 查询问题?怎么解决?"
记忆口诀
延迟加载靠代理,代理拦截 getter 触发查;resultMap + select 子查询,fetchType="lazy" 是开关;不访问不查询,用了才触发,小心循环里 N+1。
总结
MyBatis 支持延迟加载,通过在 resultMap 中配置 fetchType="lazy" + select 子查询实现。底层原理是使用动态代理(Javassist / CGLIB)创建关联对象的代理,拦截 getter 方法调用,在真正访问关联属性时才触发 SQL 查询。需要注意 N+1 问题和 SqlSession 生命周期管理。