MyBatis延迟加载

5 阅读4分钟

MyBatis 延迟加载 (Lazy Loading) 是 MyBatis 提供的一种查询优化机制。它的核心思想是:只有在真正需要使用数据时,才去数据库查询,而不是在主查询时就把所有关联数据一次性查出来。

通俗来说就是:“按需分配,吃多少拿多少”。

以下从定义、配置、原理、优缺点四个方面详细讲解。


1. 什么是延迟加载?

在进行多表关联查询(如一对一、一对多)时,通常分两步:

  1. 主查询:查询主表信息(例如:查询用户 User)。
  2. 子查询:查询关联表信息(例如:查询该用户下的订单 Order)。
  • 立即加载 (Eager Loading):执行主查询时,立刻执行子查询。不管你后面用不用订单数据,SQL 都会把它们查出来。
  • 延迟加载 (Lazy Loading):执行主查询时,只查 User。只有当你调用 user.getOrders() 时,MyBatis 才会发起第二次 SQL 去查 Order

2. 如何配置延迟加载?

延迟加载可以通过全局配置局部配置来实现。

(1) 全局配置 (mybatis-config.xml 或 Spring Boot 配置)

在 MyBatis 配置文件中设置:

<settings>
    <!-- 开启延迟加载全局开关 (默认 false) -->
    <setting name="lazyLoadingEnabled" value="true"/>
    
    <!-- 
      关闭积极加载 (默认 false,3.4.1 版本之前默认为 true)。
      如果是 true:访问对象的任意属性(如 user.getName()),都会触发加载所有的延迟加载属性。
      如果是 false:只有访问延迟加载属性(如 user.getOrders()),才会真正触发加载。
    -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

Spring Boot (application.yml) 方式:

mybatis:
  configuration:
    lazy-loading-enabled: true
    aggressive-lazy-loading: false

(2) 局部配置 (Mapper XML) - 推荐

即使全局关闭了延迟加载,你也可以在 Mapper XML 的 <association><collection> 标签中单独指定。

  • fetchType="lazy":延迟加载
  • fetchType="eager":立即加载
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    
    <!-- 一对多关联 -->
    <collection property="orderList" 
                ofType="Order" 
                select="com.example.mapper.OrderMapper.selectByUserId" 
                column="id" 
                fetchType="lazy"> <!-- 局部指定延迟加载 -->
    </collection>
</resultMap>

3. 实现原理 (面试重点)

MyBatis 的延迟加载本质上是利用了 动态代理 (Dynamic Proxy) 设计模式。

  1. 创建代理对象: 当你开启延迟加载并执行主查询(查 User)时,MyBatis 返回的不是原生的 User 对象,而是一个 CGLIBJavassist 创建的代理对象(Proxy)。这个对象仅仅包含了 User 的基本属性,关联的 orderList 属性是空的(或者占位符)。

  2. 拦截方法调用: 当你调用 getter 方法(如 user.getOrders())时,代理对象的拦截器(Interceptor)会捕获这个调用。

  3. 判断是否已加载: 拦截器会检查 orderList 是否已经被加载过。

    • 如果是 null (未加载),则执行预定义的 SQL (select * from orders where user_id = ?)。
    • 如果已加载,直接返回数据。
  4. 填充并返回: SQL 执行完成后,通过反射将结果赋值给对象的属性,然后返回给调用者。

注意:默认情况下,调用 equals, clone, hashCode, toString 方法也会触发延迟加载(因为这些方法通常需要对象的完整状态)。可以通过 <setting name="lazyLoadTriggerMethods" ... /> 修改此行为。


4. 延迟加载的优缺点

优点

  1. 减少内存消耗:不需要的数据不加载,节省 JVM 内存。
  2. 加快首屏响应速度:主查询 SQL 简单,执行快。如果用户只看主表信息,不需要等待关联表查询。
  3. 减少数据库压力:在不需要关联数据的情况下,完全避免了多余的 SQL 查询。

缺点

  1. N+1 问题: 如果你查询了一个包含 100 个用户的列表,并且遍历这个列表去访问每个用户的订单。
    • 1 次主查询查出 100 个用户。
    • 循环中触发 100 次子查询查订单。
    • 总共执行 101 条 SQL。这会严重拖慢数据库性能。对于列表查询,通常建议使用**关联查询(Join)**一次性查出。
  2. 序列化问题: 如果你将查询结果直接转为 JSON(例如返回给前端),序列化工具(如 Jackson)会遍历对象的所有属性。这会强制触发所有的延迟加载,导致瞬间执行大量 SQL,甚至可能因为 SqlSession 已经关闭而报错(LazyInitializationException 类似异常)。

5. 什么时候使用?

  • 使用场景

    • 对象包含大字段(如文章的内容 content,列表页不显示,详情页才显示)。
    • 一对多关联,且关联数据量较大,且在当前业务逻辑中大概率用不到。
    • 详情页展示(Detail View)。
  • 不建议场景

    • 列表页展示(List View),且列表页就需要展示关联信息。
    • 批处理任务。
    • 需要将对象序列化发送到其他系统时。

总结

MyBatis 延迟加载通过动态代理拦截属性访问,实现“用时再查”。它能优化性能,但在处理列表查询JSON 序列化时需格外小心,防止引发 N+1 问题或 Session 关闭异常。通常建议在 <collection><association> 中配合 fetchType 进行精细化控制。