Mybatis 是否支持延迟加载?实现原理是什么?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,主要想考察以下几个层面:

  1. 对核心概念的理解:你是否清晰理解 “延迟加载”(Lazy Loading)这一 ORM 框架中常见的优化模式,以及它旨在解决什么问题(N+1 查询问题)。
  2. 对 MyBatis 特性的掌握:你是否熟练使用 MyBatis 的延迟加载功能,包括其配置方式(全局与局部)和关联映射类型(association, collection)。
  3. 对底层机制的探究深度:面试官不仅仅是想知道 “是否支持”,更是想知道其实现原理。这是区分 “仅会用” 和 “理解本质” 的关键,考察你是否了解其基于动态代理的实现机制。
  4. 应用与权衡能力:你是否清楚在什么场景下应该使用延迟加载,以及使用不当可能带来的问题(如序列化、事务边界等),这体现了你的实战经验和系统思维。

核心答案

是的,MyBatis 支持延迟加载。

它的实现原理是:通过动态代理技术,为目标对象创建一个代理对象。当程序第一次访问代理对象的关联属性时,拦截器会触发,执行预先配置好的 SQL 查询语句,完成数据的加载,从而实现 “按需加载”。

深度解析

原理/机制

MyBatis 的延迟加载实现主要依赖于 Javassist 或 CGLib(默认通常为 Javassist)来创建动态代理。整个过程可以分解为以下几步:

  1. 创建代理对象:在完成主查询(例如查询 Order 订单)后,MyBatis 并不会立即执行关联查询(例如查询 User 用户)。相反,它会为 Order 对象中的关联属性(如 order.getUser())返回一个代理对象,而不是真实的 User 对象。
  2. 拦截触发:这个代理对象内部持有目标对象(User)的元数据(如要执行的 SQL、参数等)和一个用于执行查询的 SqlSession 或执行器引用。
  3. “懒” 加载:当应用程序代码第一次调用代理对象的方法时(例如 proxyUser.getName()),拦截逻辑会被触发。
  4. 执行查询:拦截器会利用持有的 SqlSession 去执行关联的 SQL 查询,将结果数据加载并设置到目标对象中。
  5. 替换代理:此后,代理对象内部通常会替换为已加载的真实对象,后续的调用将直接作用于真实对象。

关键点:触发加载的时机是 “第一次调用代理对象的任何方法”,而不仅仅是 getter 方法。另外,对于 collection 集合,MyBatis 会为整个集合创建代理。

代码示例与配置

1. 配置开启延迟加载(MyBatis 全局配置):

<settings>
    <!-- 启用延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 将积极的延迟加载改为按需加载(3.4.1 后默认即为 false) -->
    <setting name="aggressiveLazyLoading" value="false"/>
    <!-- 指定使用什么代理工具,可选 JAVASSIST (默认) 或 CGLIB -->
    <setting name="proxyFactory" value="JAVASSIST"/>
</settings>

2. 映射文件中使用延迟加载

<resultMap id="orderWithUserLazyMap" type="com.example.Order">
    <id property="id" column="id"/>
    <result property="orderNumber" column="order_number"/>
    <!-- 对关联对象 user 进行延迟加载 -->
    <association property="user" column="user_id"
                 javaType="com.example.User"
                 select="com.example.UserMapper.selectById"
                 fetchType="lazy"/> <!-- 此处显式指定为 lazy -->
</resultMap>

<select id="selectOrderWithUserLazy" resultMap="orderWithUserLazyMap">
    SELECT * FROM orders WHERE id = #{id}
</select>

3. Java 代码中的调用

// 此时只执行了查询 order 的 SQL
Order order = orderMapper.selectOrderWithUserLazy(1);
System.out.println(order.getOrderNumber()); // 正常输出,不触发加载

// 第一次调用代理对象 user 的方法,触发执行查询 user 的 SQL
System.out.println(order.getUser().getName()); // 此处触发延迟加载

对比分析与最佳实践

  • aggressiveLazyLoading 的作用

    • 当设置为 true(旧版本默认),任何对主对象方法的调用都会导致所有延迟加载的属性被立即加载,行为过于 “积极”。
    • 设置为 false(新版本默认)后,只有当程序直接调用延迟加载属性本身的方法时,该属性才会被加载,这才是真正的 “按需加载”。
  • 适用场景

    • 推荐使用:在复杂的对象关系网络中,特别是前端或服务层不需要立即使用所有关联数据时。例如,查看订单列表时,先不加载每条订单的详情和所有商品项,只在点击查看详情时才加载。
    • 谨慎/避免使用
      1. 事务外部访问延迟属性会导致异常,因为加载数据需要数据库连接。
      2. 对象需要被序列化(如网络传输、缓存存储)时,代理对象序列化可能有问题。
      3. 在 Web 开发中,若在视图层(如 JSP)才触发加载,而此时 SqlSession 已关闭,会引发 LazyInitializationException(在集成 Spring 且事务管理得当时可避免)。

常见误区

  1. 认为 fetchType="lazy" 是万能优化:在不必要的情况下使用,反而会因为频繁创建代理和发起多次数据库连接/请求而降低性能。对于确定立即需要的数据,应使用急加载(fetchType="eager")或联表查询。
  2. 混淆全局与局部配置:局部映射中的 fetchType 属性(lazy/eager)优先级高于全局的 lazyLoadingEnabled 设置。
  3. 忽视事务边界:这是最常见的陷阱。必须确保在延迟加载触发时,操作仍处在有效的数据库事务生命周期内,否则会报错。

总结

MyBatis 通过动态代理机制实现延迟加载,它是一种用 “空间(代理对象)换时间(查询执行)” 的优化策略,核心价值在于避免 N+1 查询问题,但必须结合具体业务场景和事务管理谨慎使用,否则可能适得其反。