MyBatis 延迟加载 (Lazy Loading) 是 MyBatis 提供的一种查询优化机制。它的核心思想是:只有在真正需要使用数据时,才去数据库查询,而不是在主查询时就把所有关联数据一次性查出来。
通俗来说就是:“按需分配,吃多少拿多少”。
以下从定义、配置、原理、优缺点四个方面详细讲解。
1. 什么是延迟加载?
在进行多表关联查询(如一对一、一对多)时,通常分两步:
- 主查询:查询主表信息(例如:查询用户
User)。 - 子查询:查询关联表信息(例如:查询该用户下的订单
Order)。
- 立即加载 (Eager Loading):执行主查询时,立刻执行子查询。不管你后面用不用订单数据,SQL 都会把它们查出来。
- 延迟加载 (Lazy Loading):执行主查询时,只查
User。只有当你调用user.getOrders()时,MyBatis 才会发起第二次 SQL 去查Order。
2. 如何配置延迟加载?
延迟加载可以通过全局配置和局部配置来实现。
(1) 全局配置 (mybatis-config.xml 或 Spring Boot 配置)
在 MyBatis 配置文件中设置:
<settings>
<!-- 开启延迟加载全局开关 (默认 false) -->
<setting name="lazyLoadingEnabled" value="true"/>
<!--
关闭积极加载 (默认 false,3.4.1 版本之前默认为 true)。
如果是 true:访问对象的任意属性(如 user.getName()),都会触发加载所有的延迟加载属性。
如果是 false:只有访问延迟加载属性(如 user.getOrders()),才会真正触发加载。
-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
Spring Boot (application.yml) 方式:
mybatis:
configuration:
lazy-loading-enabled: true
aggressive-lazy-loading: false
(2) 局部配置 (Mapper XML) - 推荐
即使全局关闭了延迟加载,你也可以在 Mapper XML 的 <association> 或 <collection> 标签中单独指定。
fetchType="lazy":延迟加载fetchType="eager":立即加载
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 一对多关联 -->
<collection property="orderList"
ofType="Order"
select="com.example.mapper.OrderMapper.selectByUserId"
column="id"
fetchType="lazy"> <!-- 局部指定延迟加载 -->
</collection>
</resultMap>
3. 实现原理 (面试重点)
MyBatis 的延迟加载本质上是利用了 动态代理 (Dynamic Proxy) 设计模式。
-
创建代理对象: 当你开启延迟加载并执行主查询(查 User)时,MyBatis 返回的不是原生的
User对象,而是一个 CGLIB 或 Javassist 创建的代理对象(Proxy)。这个对象仅仅包含了 User 的基本属性,关联的orderList属性是空的(或者占位符)。 -
拦截方法调用: 当你调用 getter 方法(如
user.getOrders())时,代理对象的拦截器(Interceptor)会捕获这个调用。 -
判断是否已加载: 拦截器会检查
orderList是否已经被加载过。- 如果是 null (未加载),则执行预定义的 SQL (
select * from orders where user_id = ?)。 - 如果已加载,直接返回数据。
- 如果是 null (未加载),则执行预定义的 SQL (
-
填充并返回: SQL 执行完成后,通过反射将结果赋值给对象的属性,然后返回给调用者。
注意:默认情况下,调用
equals,clone,hashCode,toString方法也会触发延迟加载(因为这些方法通常需要对象的完整状态)。可以通过<setting name="lazyLoadTriggerMethods" ... />修改此行为。
4. 延迟加载的优缺点
优点
- 减少内存消耗:不需要的数据不加载,节省 JVM 内存。
- 加快首屏响应速度:主查询 SQL 简单,执行快。如果用户只看主表信息,不需要等待关联表查询。
- 减少数据库压力:在不需要关联数据的情况下,完全避免了多余的 SQL 查询。
缺点
- N+1 问题:
如果你查询了一个包含 100 个用户的列表,并且遍历这个列表去访问每个用户的订单。
- 1 次主查询查出 100 个用户。
- 循环中触发 100 次子查询查订单。
- 总共执行 101 条 SQL。这会严重拖慢数据库性能。对于列表查询,通常建议使用**关联查询(Join)**一次性查出。
- 序列化问题:
如果你将查询结果直接转为 JSON(例如返回给前端),序列化工具(如 Jackson)会遍历对象的所有属性。这会强制触发所有的延迟加载,导致瞬间执行大量 SQL,甚至可能因为
SqlSession已经关闭而报错(LazyInitializationException类似异常)。
5. 什么时候使用?
-
使用场景:
- 对象包含大字段(如文章的内容
content,列表页不显示,详情页才显示)。 - 一对多关联,且关联数据量较大,且在当前业务逻辑中大概率用不到。
- 详情页展示(Detail View)。
- 对象包含大字段(如文章的内容
-
不建议场景:
- 列表页展示(List View),且列表页就需要展示关联信息。
- 批处理任务。
- 需要将对象序列化发送到其他系统时。
总结
MyBatis 延迟加载通过动态代理拦截属性访问,实现“用时再查”。它能优化性能,但在处理列表查询和JSON 序列化时需格外小心,防止引发 N+1 问题或 Session 关闭异常。通常建议在 <collection> 或 <association> 中配合 fetchType 进行精细化控制。