Mybatis 是否支持延迟加载?实现原理是什么?
2026年01月07日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/
面试考察点
面试官提出这个问题,主要想考察以下几个层面:
- 对核心概念的理解:你是否清晰理解 “延迟加载”(Lazy Loading)这一 ORM 框架中常见的优化模式,以及它旨在解决什么问题(N+1 查询问题)。
- 对 MyBatis 特性的掌握:你是否熟练使用 MyBatis 的延迟加载功能,包括其配置方式(全局与局部)和关联映射类型(
association,collection)。 - 对底层机制的探究深度:面试官不仅仅是想知道 “是否支持”,更是想知道其实现原理。这是区分 “仅会用” 和 “理解本质” 的关键,考察你是否了解其基于动态代理的实现机制。
- 应用与权衡能力:你是否清楚在什么场景下应该使用延迟加载,以及使用不当可能带来的问题(如序列化、事务边界等),这体现了你的实战经验和系统思维。
核心答案
是的,MyBatis 支持延迟加载。
它的实现原理是:通过动态代理技术,为目标对象创建一个代理对象。当程序第一次访问代理对象的关联属性时,拦截器会触发,执行预先配置好的 SQL 查询语句,完成数据的加载,从而实现 “按需加载”。
深度解析
原理/机制
MyBatis 的延迟加载实现主要依赖于 Javassist 或 CGLib(默认通常为 Javassist)来创建动态代理。整个过程可以分解为以下几步:
- 创建代理对象:在完成主查询(例如查询
Order订单)后,MyBatis 并不会立即执行关联查询(例如查询User用户)。相反,它会为Order对象中的关联属性(如order.getUser())返回一个代理对象,而不是真实的User对象。 - 拦截触发:这个代理对象内部持有目标对象(
User)的元数据(如要执行的 SQL、参数等)和一个用于执行查询的SqlSession或执行器引用。 - “懒” 加载:当应用程序代码第一次调用代理对象的方法时(例如
proxyUser.getName()),拦截逻辑会被触发。 - 执行查询:拦截器会利用持有的
SqlSession去执行关联的 SQL 查询,将结果数据加载并设置到目标对象中。 - 替换代理:此后,代理对象内部通常会替换为已加载的真实对象,后续的调用将直接作用于真实对象。
关键点:触发加载的时机是 “第一次调用代理对象的任何方法”,而不仅仅是 getter 方法。另外,对于 collection 集合,MyBatis 会为整个集合创建代理。
代码示例与配置
1. 配置开启延迟加载(MyBatis 全局配置):
<settings>
<!-- 启用延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极的延迟加载改为按需加载(3.4.1 后默认即为 false) -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 指定使用什么代理工具,可选 JAVASSIST (默认) 或 CGLIB -->
<setting name="proxyFactory" value="JAVASSIST"/>
</settings>
2. 映射文件中使用延迟加载:
<resultMap id="orderWithUserLazyMap" type="com.example.Order">
<id property="id" column="id"/>
<result property="orderNumber" column="order_number"/>
<!-- 对关联对象 user 进行延迟加载 -->
<association property="user" column="user_id"
javaType="com.example.User"
select="com.example.UserMapper.selectById"
fetchType="lazy"/> <!-- 此处显式指定为 lazy -->
</resultMap>
<select id="selectOrderWithUserLazy" resultMap="orderWithUserLazyMap">
SELECT * FROM orders WHERE id = #{id}
</select>
3. Java 代码中的调用:
// 此时只执行了查询 order 的 SQL
Order order = orderMapper.selectOrderWithUserLazy(1);
System.out.println(order.getOrderNumber()); // 正常输出,不触发加载
// 第一次调用代理对象 user 的方法,触发执行查询 user 的 SQL
System.out.println(order.getUser().getName()); // 此处触发延迟加载
对比分析与最佳实践
-
aggressiveLazyLoading的作用:- 当设置为
true(旧版本默认),任何对主对象方法的调用都会导致所有延迟加载的属性被立即加载,行为过于 “积极”。 - 设置为
false(新版本默认)后,只有当程序直接调用延迟加载属性本身的方法时,该属性才会被加载,这才是真正的 “按需加载”。
- 当设置为
-
适用场景:
- 推荐使用:在复杂的对象关系网络中,特别是前端或服务层不需要立即使用所有关联数据时。例如,查看订单列表时,先不加载每条订单的详情和所有商品项,只在点击查看详情时才加载。
- 谨慎/避免使用:
- 在事务外部访问延迟属性会导致异常,因为加载数据需要数据库连接。
- 对象需要被序列化(如网络传输、缓存存储)时,代理对象序列化可能有问题。
- 在 Web 开发中,若在视图层(如 JSP)才触发加载,而此时 SqlSession 已关闭,会引发
LazyInitializationException(在集成 Spring 且事务管理得当时可避免)。
常见误区
- 认为
fetchType="lazy"是万能优化:在不必要的情况下使用,反而会因为频繁创建代理和发起多次数据库连接/请求而降低性能。对于确定立即需要的数据,应使用急加载(fetchType="eager")或联表查询。 - 混淆全局与局部配置:局部映射中的
fetchType属性(lazy/eager)优先级高于全局的lazyLoadingEnabled设置。 - 忽视事务边界:这是最常见的陷阱。必须确保在延迟加载触发时,操作仍处在有效的数据库事务生命周期内,否则会报错。
总结
MyBatis 通过动态代理机制实现延迟加载,它是一种用 “空间(代理对象)换时间(查询执行)” 的优化策略,核心价值在于避免 N+1 查询问题,但必须结合具体业务场景和事务管理谨慎使用,否则可能适得其反。