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 从数据库里查到一个主对象(比如 Order)时,如果它发现这个对象有配置了延迟加载的关联属性(比如 User 或 List),它并不会直接把这些关联数据也查出来。相反,它会给这些关联属性生成一个“替身”,也就是一个动态代理对象。
这个代理对象,有点像一个“空壳子”,它实现了原始关联对象的接口或者继承了原始关联对象的类。当你首次尝试访问这个代理对象的任何方法时(比如 order.getUser().getName()),这个代理对象就会“醒过来”。它会拦截你的方法调用,然后触发 MyBatis 去执行之前在 Mapper XML 里配置好的那个 select 语句(比如 selectUserById),真正地从数据库里把关联数据加载进来。数据加载完成后,这个代理对象会把真实的数据填充进去,或者将后续的调用委托给这个真实的对象。
整个过程对开发者来说几乎是透明的,你感觉就像直接操作真实对象一样。这种机制,避免了在不需要关联数据时就进行额外的数据库查询,从而显著提升了初始查询的性能。当然,这一切都离不开 SqlSession 的功劳,它得保持活跃,才能在需要时触发这些后续查询。如果 SqlSession 提前关闭了,那这个代理对象就“失灵”了,再访问它就会出问题。
配置方面,前面其实已经提到了核心点: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 会在后台帮你处理好一切。
延迟加载这东西,用好了是神器,用不好也可能挖坑。
优点:
这是最直接的。它减少了初始查询的数据量和数据库交互次数。比如你查一个订单列表,但大多数时候并不需要立即知道每个订单的具体用户或商品详情,延迟加载就能让你快速拿到列表,只有当用户点击某个订单查看详情时,才去加载那些关联数据。缺点与注意事项:
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(这通常很复杂)。总的来说,延迟加载是一个强大的工具,但它要求你对数据访问模式有清晰的理解。不是所有关联数据都适合延迟加载,关键在于平衡初始加载速度和后续数据访问的效率。