基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

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主要使用两种动态代理技术:

  1. CGLIB(默认):通过继承目标类,动态创建一个子类作为代理。这是默认的代理方式,因为它不需要目标类实现任何接口。
  2. Javassist:一个用于字节码操作的库,功能与CGLIB类似,MyBatis也支持用它来创建代理。

整个流程如下:

  1. 返回代理对象:当你执行一个配置了懒加载的查询(例如 selectUserById),MyBatis并不会返回一个普通的 User 对象。相反,它会使用CGLIB或Javassist在运行时动态地创建一个User的子类(我们称之为UserProxy),并返回这个子类的实例。

  2. 代理对象持有必要信息:这个UserProxy对象内部除了包含已经从数据库查出的User基本信息(如id, name)外,还持有一个“拦截器”(Interceptor)。这个拦截器知道如何去加载那些需要懒加载的属性(比如orders列表),它包含了执行后续查询所需的信息,如:

    • 要执行的SQL语句ID(例如 com.example.mapper.OrderMapper.selectOrdersByUserId)。
    • 传入该SQL的参数(例如 userId)。
  3. 拦截方法调用UserProxy重写了父类(也就是User类)的所有方法。当你调用userProxy.getOrders()时,这个调用会被代理对象拦截。

  4. 触发加载:拦截器发现你正在访问一个尚未加载的懒加载属性(orders)。

    • 它会检查这个属性是否已经被加载过。
    • 如果是第一次访问,拦截器会使用它之前保存的SQL语句ID和参数,向数据库发起一次新的查询(去查询订单)。
    • 查询完成后,将返回的订单列表List<Order>设置到真正的User对象(代理对象内部持有的)的orders属性中。
    • 最后,将加载好的数据返回给你。
  5. 后续访问:当你再次调用userProxy.getOrders()时,拦截器会发现orders属性已经有数据了,于是直接返回数据,不再执行数据库查询。


3. 如何在MyBatis中配置和使用懒加载

步骤1:全局配置开启懒加载

在MyBatis的全局配置文件 mybatis-config.xml 中进行设置。

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

java
public class User {
    private Integer id;
    private String username;
    private List<Order> orders;
    // getters and setters...
}

Order.java

java
public class Order {
    private Integer id;
    private String orderNumber;
    // getters and setters...
}

UserMapper.xml

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

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代码中验证

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. 注意事项和潜在问题

  1. N+1问题:懒加载虽然好,但如果使用不当,会引发经典的“N+1”问题。

    • 场景:查询了N个用户列表 List<User> users,然后循环遍历这个列表,并在循环中调用 user.getOrders()
    • 结果:会先执行1次查询用户列表的SQL,然后在循环中,为每个用户分别执行1次查询订单的SQL,总共执行了 N+1 次数据库查询,性能极差。
    • 解决方案:对于这种明确需要批量加载关联数据的场景,应该放弃懒加载,直接使用JOIN查询,在一次SQL中将所有需要的数据全部查出。
  2. SqlSession必须保持开启:懒加载的后续查询需要通过SqlSession来执行。如果你在触发懒加载(调用getOrders())之前关闭了SqlSession,程序会抛出LazyInitializationException异常。这在Web开发中尤其需要注意,要确保SqlSession的生命周期覆盖了整个业务逻辑处理过程。

总结

MyBatis通过动态代理技术实现了懒加载。它返回一个代理对象来代替真实的POJO。当访问被标记为懒加载的属性时,代理对象会拦截该调用,执行必要的数据库查询来获取数据,并将其填充到真实对象中。这种机制可以有效提升应用性能,但需要开发者注意避免N+1问题,并正确管理SqlSession的生命周期。

00:00
00:00