MyBatis是如何实现懒加载(Lazy Loading)的?
MyBatis通过动态代理(CGLIB)实现懒加载。它返回一个代理对象,当首次访问某个懒加载属性时,代理会拦截调用并触发一次新的数据库查询,实现按需加载,提升性能。
我们来详细剖析一下MyBatis是如何实现懒加载(Lazy Loading)的。
1. 什么是懒加载?
懒加载是一种设计模式,也称为“按需加载”。它的核心思想是:当真正需要数据的时候,才去执行数据库查询,而不是在加载主对象时就立即加载所有关联的对象。
举个例子:
查询一个用户(User)的信息时,这个用户可能关联了大量的订单(Order)。
- 非懒加载:查询
User时,会立即执行一条复杂的JOIN语句,把该用户的所有Order信息也一并查出来,即使你当前的代码逻辑根本用不到这些订单信息。 - 懒加载:查询
User时,只执行查询User表的基本SQL。User对象中的订单列表(List<Order>)此时是一个“占位符”或者说“代理对象”。只有当你第一次调用user.getOrders()方法时,MyBatis才会去数据库执行查询订单的SQL,并将结果填充到User对象中。
懒加载的优点:
- 提升性能:避免了不必要的数据库查询,特别是对于一对多、多对多的关联关系,可以显著减少首次加载的开销。
- 节省内存:只有在需要时才加载关联数据,减少了内存占用。
2. MyBatis懒加载的核心实现原理:动态代理
MyBatis实现懒加载的核心技术是动态代理(Dynamic Proxy)。它不直接返回你期望的POJO(Plain Old Java Object)实例,而是返回一个该POJO的代理对象。
MyBatis主要使用两种动态代理技术:
- CGLIB(默认):通过继承目标类,动态创建一个子类作为代理。这是默认的代理方式,因为它不需要目标类实现任何接口。
- Javassist:一个用于字节码操作的库,功能与CGLIB类似,MyBatis也支持用它来创建代理。
整个流程如下:
返回代理对象:当你执行一个配置了懒加载的查询(例如
selectUserById),MyBatis并不会返回一个普通的User对象。相反,它会使用CGLIB或Javassist在运行时动态地创建一个User的子类(我们称之为UserProxy),并返回这个子类的实例。代理对象持有必要信息:这个
UserProxy对象内部除了包含已经从数据库查出的User基本信息(如id, name)外,还持有一个“拦截器”(Interceptor)。这个拦截器知道如何去加载那些需要懒加载的属性(比如orders列表),它包含了执行后续查询所需的信息,如:- 要执行的SQL语句ID(例如
com.example.mapper.OrderMapper.selectOrdersByUserId)。 - 传入该SQL的参数(例如
userId)。
- 要执行的SQL语句ID(例如
拦截方法调用:
UserProxy重写了父类(也就是User类)的所有方法。当你调用userProxy.getOrders()时,这个调用会被代理对象拦截。触发加载:拦截器发现你正在访问一个尚未加载的懒加载属性(
orders)。- 它会检查这个属性是否已经被加载过。
- 如果是第一次访问,拦截器会使用它之前保存的SQL语句ID和参数,向数据库发起一次新的查询(去查询订单)。
- 查询完成后,将返回的订单列表
List<Order>设置到真正的User对象(代理对象内部持有的)的orders属性中。 - 最后,将加载好的数据返回给你。
后续访问:当你再次调用
userProxy.getOrders()时,拦截器会发现orders属性已经有数据了,于是直接返回数据,不再执行数据库查询。
3. 如何在MyBatis中配置和使用懒加载
步骤1:全局配置开启懒加载
在MyBatis的全局配置文件 mybatis-config.xml 中进行设置。
<settings>
<!-- 全局懒加载开关,默认为false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式懒加载开关,默认为false -->
<!-- 设为true时,任何对代理对象的方法调用(除了equals,clone,hashCode,toString)都会触发所有懒加载属性的加载 -->
<!-- 强烈建议保持为false,实现真正的按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 指定哪个方法会触发懒加载,默认为equals,clone,hashCode,toString -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
lazyLoadingEnabled: 必须设为true才能开启懒加载功能。aggressiveLazyLoading: 这个配置非常重要。false(默认和推荐):只有当你调用特定懒加载属性的getter方法时(如getOrders()),才会加载这个属性。这是真正的“懒加载”。true:只要你调用代理对象的任何方法(比如user.getName()),就会立即触发所有懒加载属性的加载。这会让懒加载失去意义,所以通常保持为false。
步骤2:在Mapper XML中指定关联查询
在 ResultMap 中,通过 <association> (一对一) 或 <collection> (一对多) 标签来配置关联关系,并使用 fetchType="lazy" 来指定该关联属性使用懒加载。
示例:一个用户(User)关联多个订单(Order)
User.java
public class User {
private Integer id;
private String username;
private List<Order> orders;
// getters and setters...
}
Order.java
public class Order {
private Integer id;
private String orderNumber;
// getters and setters...
}
UserMapper.xml
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserResultMap" type="com.example.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!--
配置一对多关联:
- property="orders": 对应User类中的orders属性
- ofType="com.example.Order": 列表中的元素类型
- select="com.example.mapper.OrderMapper.findOrdersByUserId": 指定由哪个查询来加载orders数据
- column="id": 将主查询结果的'id'列作为参数传递给select指定的查询
- fetchType="lazy": 核心!指定此关联为懒加载
-->
<collection property="orders"
ofType="com.example.Order"
select="com.example.mapper.OrderMapper.findOrdersByUserId"
column="id"
fetchType="lazy"/>
</resultMap>
<select id="findUserById" resultMap="UserResultMap">
select id, username from user where id = #{id}
</select>
</mapper>
OrderMapper.xml
<mapper namespace="com.example.mapper.OrderMapper">
<select id="findOrdersByUserId" resultType="com.example.Order">
select id, order_number from `order` where user_id = #{userId}
</select>
</mapper>
注意:
column="id"的值id会作为参数传递给findOrdersByUserId方法。MyBatis会自动匹配参数名,如果findOrdersByUserId方法的参数是#{userId},它也能智能地匹配上。
步骤3:在Java代码中验证
// 获取SqlSession和Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println("--- 开始查询用户 ---");
User user = userMapper.findUserById(1); // 此时只会执行查询用户的SQL
System.out.println("--- 用户查询完成 ---");
System.out.println("用户名: " + user.getUsername()); // 不会触发懒加载
System.out.println("--- 准备触发懒加载,获取订单列表 ---");
List<Order> orders = user.getOrders(); // 第一次调用getOrders(),触发查询订单的SQL
System.out.println("--- 懒加载执行完毕 ---");
System.out.println("订单数量: " + orders.size());
System.out.println("--- 再次获取订单列表 ---");
List<Order> orders2 = user.getOrders(); // 第二次调用,直接从内存返回,不再查询数据库
System.out.println("--- 第二次获取完成 ---");
观察日志输出:
你会看到查询订单的SQL语句是在“准备触发懒加载”之后,第一次调用 user.getOrders() 时才被打印出来的。
4. 注意事项和潜在问题
N+1问题:懒加载虽然好,但如果使用不当,会引发经典的“N+1”问题。
- 场景:查询了N个用户列表
List<User> users,然后循环遍历这个列表,并在循环中调用user.getOrders()。 - 结果:会先执行1次查询用户列表的SQL,然后在循环中,为每个用户分别执行1次查询订单的SQL,总共执行了 N+1 次数据库查询,性能极差。
- 解决方案:对于这种明确需要批量加载关联数据的场景,应该放弃懒加载,直接使用
JOIN查询,在一次SQL中将所有需要的数据全部查出。
- 场景:查询了N个用户列表
SqlSession必须保持开启:懒加载的后续查询需要通过
SqlSession来执行。如果你在触发懒加载(调用getOrders())之前关闭了SqlSession,程序会抛出LazyInitializationException异常。这在Web开发中尤其需要注意,要确保SqlSession的生命周期覆盖了整个业务逻辑处理过程。
总结
MyBatis通过动态代理技术实现了懒加载。它返回一个代理对象来代替真实的POJO。当访问被标记为懒加载的属性时,代理对象会拦截该调用,执行必要的数据库查询来获取数据,并将其填充到真实对象中。这种机制可以有效提升应用性能,但需要开发者注意避免N+1问题,并正确管理SqlSession的生命周期。