MyBatis支持延迟加载,通过配置lazyLoadingEnabled=true和fetchType="lazy"实现按需加载,利用动态代理在访问关联属性时触发SQL查询,提升性能但需注意N+1查询、SqlSession生命周期和序列化问题。

MyBatis 确实支持延迟加载(Lazy Loading),而且这功能在实际项目里简直是性能优化的利器。说白了,它的核心思想就是“按需加载”——只有当你真正需要某个关联数据的时候,MyBatis 才会去数据库里把它捞出来,而不是一股脑地全部加载进来。这对于处理复杂对象图和大数据量关联查询时,能显著减少内存占用和数据库交互次数,让你的应用响应更快。
解决方案
要让 MyBatis 玩转延迟加载,主要是在配置和映射文件里做文章。首先,全局配置里得把 lazyLoadingEnabled 这个开关打开,通常它在 MyBatis 3.x 之后默认就是 true 了,但明确设置一下总是没错的。
这里 aggressiveLazyLoading 挺有意思的。当它设为 false 时,MyBatis 会尽量做到“极致”的延迟加载,只有当你访问到某个关联对象的具体属性时,才会去加载那个对象。如果设为 true,那么只要你一访问到那个代理对象本身(比如调用它的任何方法),MyBatis 就会把这个对象的所有属性都加载进来。我个人偏向于设为 false,这样才真正体现了延迟加载的精髓。
接着,在你的 Mapper XML 文件里,针对那些你希望延迟加载的 (一对一)或 (一对多)标签,加上 fetchType="lazy" 属性。
这样一来,当你查询一个 Order 对象时,MyBatis 不会立即去查 User 和 OrderItem 的数据。只有当你代码里真正去调用 order.getUser() 或者 order.getItems() 时,MyBatis 才会默默地发起新的 SQL 查询。
MyBatis延迟加载的工作原理是什么?
聊到原理,MyBatis 的延迟加载玩的是“代理”这套把戏。说白了,当 MyBatis 从数据库里查到一个主对象(比如 Order)时,如果它发现这个对象有配置了延迟加载的关联属性(比如 User 或 List),它并不会直接把这些关联数据也查出来。相反,它会给这些关联属性生成一个“替身”,也就是一个动态代理对象。
这个代理对象,有点像一个“空壳子”,它实现了原始关联对象的接口或者继承了原始关联对象的类。当你首次尝试访问这个代理对象的任何方法时(比如 order.getUser().getName()),这个代理对象就会“醒过来”。它会拦截你的方法调用,然后触发 MyBatis 去执行之前在 Mapper XML 里配置好的那个 select 语句(比如 selectUserById),真正地从数据库里把关联数据加载进来。数据加载完成后,这个代理对象会把真实的数据填充进去,或者将后续的调用委托给这个真实的对象。
整个过程对开发者来说几乎是透明的,你感觉就像直接操作真实对象一样。这种机制,避免了在不需要关联数据时就进行额外的数据库查询,从而显著提升了初始查询的性能。当然,这一切都离不开 SqlSession 的功劳,它得保持活跃,才能在需要时触发这些后续查询。如果 SqlSession 提前关闭了,那这个代理对象就“失灵”了,再访问它就会出问题。
如何在MyBatis中配置和使用延迟加载?
配置方面,前面其实已经提到了核心点:mybatis-config.xml 里的全局设置和 Mapper XML 里的 fetchType 属性。
全局配置 (mybatis-config.xml):
lazyLoadingEnabled 设为 true 是基础,它告诉 MyBatis 启用延迟加载机制。aggressiveLazyLoading 设为 false 是我个人比较推荐的,它让延迟加载更“懒”,只有当你真正访问到关联对象的某个具体属性时,才会去触发加载。如果设为 true,那么只要你一访问到那个代理对象,它就会把所有关联数据都加载进来,这在某些场景下可能就失去了延迟加载的意义。
Mapper XML 配置 ( 和 ):
这里的 fetchType="lazy" 是关键。它明确告诉 MyBatis,user 和 items 这两个属性在加载 Order 对象时,不要立即去数据库查询,而是等它们被访问时再查。select 属性指向的是一个独立的查询语句,MyBatis 会在需要时调用这个语句来获取关联数据,column 属性则提供了关联查询的参数。
使用示例:
在你的 Java 代码中,使用起来和普通对象没什么两样:
// 获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
Order order = orderMapper.selectOrderById(1L); // 此时 User 和 OrderItem 并未加载
System.out.println("订单号: " + order.getOrderNo()); // 不会触发关联加载
// 访问 user 对象,此时会触发 UserMapper.selectUserById 查询
User user = order.getUser();
System.out.println("下单用户: " + user.getName());
// 访问 items 集合,此时会触发 OrderItemMapper.selectOrderItemsByOrderId 查询
List items = order.getItems();
for (OrderItem item : items) {
System.out.println(" 商品: " + item.getProductName() + ", 数量: " + item.getQuantity());
}
} finally {
sqlSession.close(); // 关闭SqlSession
} 可以看到,代码里你无需感知到延迟加载的存在,直接调用 getUser() 和 getItems() 即可。MyBatis 会在后台帮你处理好一切。
延迟加载有哪些优缺点,以及使用时需要注意什么?
延迟加载这东西,用好了是神器,用不好也可能挖坑。
优点:
- 性能提升: 这是最直接的。它减少了初始查询的数据量和数据库交互次数。比如你查一个订单列表,但大多数时候并不需要立即知道每个订单的具体用户或商品详情,延迟加载就能让你快速拿到列表,只有当用户点击某个订单查看详情时,才去加载那些关联数据。
- 内存优化: 减少了一次性加载到内存中的数据量,特别是在处理复杂对象图时,能有效避免内存溢出。
- 带宽节省: 减少了数据库和应用服务器之间的数据传输量。
缺点与注意事项:
-
N+1 查询问题: 这是延迟加载最常遇到的坑。如果你查询了一个订单列表,然后遍历这个列表,对每个订单都去访问它的延迟加载属性(比如
order.getUser().getName()),那么对于 N 个订单,MyBatis 就会发出 N+1 次查询(1 次查订单列表,N 次查用户)。这会导致大量的数据库往返,性能反而会急剧下降。-
解决方案: 对于经常需要一起查询的关联数据,考虑使用
JOIN语句在一次查询中搞定,或者在 MyBatis 中使用fetchType="eager"。MyBatis 3.5.2 以后引入的select="someMapper.someMethod" fetchType="lazy"配合resultMap的association和collection可以很好地控制,但如果发现 N+1,还是得考虑JOIN或批量查询。
-
解决方案: 对于经常需要一起查询的关联数据,考虑使用
-
SqlSession生命周期: 前面提到了,延迟加载依赖于SqlSession的活跃状态。如果你的SqlSession在访问延迟加载属性之前就关闭了,那么你就会遇到类似LazyInitializationException(虽然 MyBatis 不会直接抛这个,但你会拿到一个未加载的代理对象,或者直接报错)的问题。这在 Web 应用中尤其常见,因为请求结束通常会关闭SqlSession。-
解决方案: 确保在访问所有延迟加载属性之前,
SqlSession仍然是打开的。在 Spring 这样的框架中,通常通过事务管理器来管理SqlSession的生命周期,确保在一个事务(或请求)的整个过程中SqlSession都是可用的。
-
解决方案: 确保在访问所有延迟加载属性之前,
-
序列化问题: 如果你尝试序列化一个包含延迟加载代理对象的实体,而代理对象内部的真实数据尚未加载,那么在反序列化之后,这个代理对象可能就无法正常工作了,因为它失去了与
SqlSession的连接。-
解决方案: 在序列化之前强制加载所有关联数据,或者在反序列化之后重新关联
SqlSession(这通常很复杂)。
-
解决方案: 在序列化之前强制加载所有关联数据,或者在反序列化之后重新关联
- 理解成本: 虽然用起来透明,但理解其背后的代理机制和生命周期管理,对于排查问题和优化性能至关重要。
总的来说,延迟加载是一个强大的工具,但它要求你对数据访问模式有清晰的理解。不是所有关联数据都适合延迟加载,关键在于平衡初始加载速度和后续数据访问的效率。










