MyBatis 延迟加载深度解密:从使用方式到底层动态代理原理解析

 更新时间:2026年06月03日 09:16:16   作者:Mahir08  
本文详细解析MyBatis延迟加载机制,涵盖定义、配置方式、底层原理、执行流程及常见坑点,助你掌握延迟加载优化数据库查询与内存使用技巧,应对面试与实际项目挑战,感兴趣的朋友一起看看吧

作为 Java 后端开发者,我们在使用 MyBatis 处理关联查询时,经常会遇到一个问题:查询一个主对象时,会同时查询出所有关联对象,即使这些关联对象我们根本用不到。这不仅会导致不必要的数据库查询,还会占用大量内存,影响系统性能。

 延迟加载(懒加载) 正是解决这个问题的最佳方案。它允许我们在真正需要使用关联对象时,才去执行 SQL 查询加载数据,而不是一次性加载所有关联数据。

面试时,MyBatis 延迟加载更是高频考点,面试官会层层深挖:

  • MyBatis 支持延迟加载吗?哪些关联查询支持延迟加载?
  • 延迟加载的底层原理是什么?基于什么设计模式?
  • lazyLoadingEnabledaggressiveLazyLoading有什么区别?
  • 为什么嵌套结果不支持延迟加载?
  • 延迟加载有哪些常见坑点?如何避免?

这篇文章,我们就从基础使用→配置方式→底层原理→执行流程→坑点与最佳实践五个维度,彻底搞懂 MyBatis 延迟加载。不仅会讲清楚理论,更会结合源码和实战案例,让你看完既能轻松应对面试,又能在实际项目中正确使用延迟加载。

一、先搞懂:什么是延迟加载?

1. 延迟加载的定义

延迟加载(Lazy Loading),也叫懒加载,是一种按需加载的设计思想。它的核心是:只有当真正需要使用某个对象时,才会去加载这个对象的数据

在 MyBatis 中,延迟加载主要用于处理关联查询(一对一、一对多、多对多)。当我们查询主对象时,不会立即查询关联对象的数据,而是为关联对象生成一个代理对象。只有当我们调用关联对象的 getter 方法时,才会触发真正的 SQL 查询,加载关联对象的数据。

2. 为什么需要延迟加载?

我们用一个最常见的场景来对比:查询用户信息,同时查询用户的订单信息。

不使用延迟加载(立即加载)

-- 一次性查询用户和所有订单
SELECT u.*, o.* FROM user u LEFT JOIN order o ON u.id = o.user_id WHERE u.id = 1

这种方式的问题:

  • 如果用户有 1000 个订单,会一次性查询出所有订单数据
  • 如果我们只需要用户的基本信息,不需要订单信息,这些订单查询就是完全浪费的
  • 数据量越大,性能损耗越严重,甚至会导致内存溢出

使用延迟加载

-- 第一步:只查询用户基本信息
SELECT * FROM user WHERE id = 1
-- 第二步:只有当调用user.getOrders()时,才执行订单查询
SELECT * FROM order WHERE user_id = 1

这种方式的优势:

  • 只查询需要的数据,减少不必要的数据库查询
  • 降低内存占用,提高系统性能
  • 对于关联对象不常访问的场景,性能提升非常明显

3. MyBatis 对延迟加载的支持

MyBatis 是支持延迟加载的,但只支持嵌套查询(也叫子查询)的延迟加载,不支持嵌套结果的延迟加载

这是一个非常重要的结论,也是面试最常考的点。很多人以为所有关联查询都支持延迟加载,其实不然。

关联查询方式是否支持延迟加载原理
嵌套查询(select属性)先查询主表,关联对象用代理对象代替,需要时再执行子查询
嵌套结果(resultMap嵌套)一次性执行多表联查,将结果映射到主对象和关联对象

二、延迟加载的配置方式

MyBatis 提供了两种配置延迟加载的方式:全局配置局部配置。局部配置的优先级高于全局配置。

1. 全局配置

mybatis-config.xml文件中配置全局延迟加载参数:

<settings>
    <!-- 开启全局延迟加载,默认值为false -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭侵入式延迟加载,默认值为false(MyBatis 3.4.1及以后) -->
    <setting name="aggressiveLazyLoading" value="false"/>
    <!-- 指定延迟加载使用的动态代理工厂,默认是cglib -->
    <setting name="proxyFactory" value="cglib"/>
</settings>

两个核心参数详解

  • lazyLoadingEnabled:全局延迟加载开关。设置为true时,所有关联查询都会默认使用延迟加载;设置为false时,所有关联查询都会立即加载。
  • aggressiveLazyLoading:侵入式延迟加载开关。这个参数非常重要,很多人搞不清它的作用:
    • 当设置为true时:调用主对象的任何方法(如toString()hashCode()equals())都会触发所有延迟加载属性的加载
    • 当设置为false时:只有调用对应延迟加载属性的 getter 方法时,才会触发该属性的加载

注意:MyBatis 3.4.1 及以后版本,aggressiveLazyLoading的默认值已经改为false,这是更合理的默认行为。在这之前的版本,默认值是true

2. 局部配置

如果不想全局开启延迟加载,可以在单个关联查询上通过fetchType属性单独配置:

<resultMap id="userResultMap" type="User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!-- 一对一关联,开启延迟加载 -->
    <association property="dept" column="dept_id" 
                 select="com.example.mapper.DeptMapper.getDeptById"
                 fetchType="lazy"/>
    <!-- 一对多关联,关闭延迟加载(立即加载) -->
    <collection property="orders" column="id"
                select="com.example.mapper.OrderMapper.getOrdersByUserId"
                fetchType="eager"/>
</resultMap>

fetchType属性有两个可选值:

  • lazy:延迟加载
  • eager:立即加载

局部配置的优先级高于全局配置。即使全局关闭了延迟加载,局部设置fetchType="lazy"仍然会生效。

三、延迟加载的底层原理:动态代理

MyBatis 延迟加载的核心原理是动态代理。当 MyBatis 发现某个关联属性需要延迟加载时,不会直接实例化这个属性,而是为它生成一个代理对象。当调用这个代理对象的 getter 方法时,才会触发真正的 SQL 查询,加载真实数据。

1. 动态代理的选择

MyBatis 提供了两种动态代理实现,通过proxyFactory参数指定:

  • CGLIB 动态代理:默认实现,基于继承实现,可以代理任何非 final 类
  • Javassist 动态代理:基于字节码生成实现,性能比 CGLIB 略高

为什么默认使用 CGLIB? 因为我们要代理的是实体类,而不是接口。JDK 动态代理只能代理接口,无法代理普通类,所以 MyBatis 选择了 CGLIB 和 Javassist 这两种可以代理类的动态代理实现。

2. 核心接口:ProxyFactory

MyBatis 定义了ProxyFactory接口,用于生成代理对象:

public interface ProxyFactory {
    // 为目标对象生成代理
    Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
}

默认实现是CglibProxyFactory,它使用 CGLIB 的Enhancer类生成代理对象。

3. 代理对象的生成过程

当 MyBatis 处理结果集时,如果发现某个关联属性配置了延迟加载,会执行以下步骤:

  1. 不直接实例化关联对象
  2. 创建一个ResultLoader对象,保存关联查询的 SQL 语句、参数和 Mapper 信息
  3. 调用ProxyFactory.createProxy()方法,为关联对象生成一个代理对象
  4. 将代理对象设置到主对象的对应属性上

4. 代理对象的执行流程

当我们调用代理对象的 getter 方法时,会触发代理对象的拦截器方法,执行以下流程:

  1. 检查关联对象是否已经加载
  2. 如果没有加载,调用ResultLoader.loadResult()方法执行 SQL 查询
  3. 将查询得到的真实对象替换代理对象
  4. 返回真实对象的对应方法结果

核心源码(CglibProxyFactory)

public static class CglibMethodInterceptor implements MethodInterceptor {
    private final Object target;
    private final ResultLoaderMap lazyLoader;
    private final Configuration configuration;
    private final ObjectFactory objectFactory;
    private final List<Class<?>> constructorArgTypes;
    private final List<Object> constructorArgs;
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 检查是否是延迟加载的属性的getter方法
        if (lazyLoader != null && !lazyLoader.isLoaded()) {
            // 如果是getter方法,触发加载
            if (isGetter(method)) {
                lazyLoader.load();
            }
            // 如果aggressiveLazyLoading为true,任何方法调用都触发所有延迟加载
            else if (configuration.isAggressiveLazyLoading()) {
                lazyLoader.loadAll();
            }
        }
        // 执行真实对象的方法
        return methodProxy.invoke(target, args);
    }
}

四、延迟加载的完整执行流程

我们以查询用户及其订单为例,完整拆解延迟加载的执行流程:

步骤 1:执行主查询

User user = userMapper.getUserById(1L);

MyBatis 执行主查询 SQL:

SELECT * FROM user WHERE id = 1

步骤 2:结果集映射

DefaultResultSetHandler处理结果集,映射 User 对象的基本属性。当处理到orders属性时:

  1. 发现orders配置了延迟加载
  2. 创建ResultLoader对象,保存订单查询的信息:
    • Mapper 方法:com.example.mapper.OrderMapper.getOrdersByUserId
    • 参数:1(用户 ID)
  3. 调用CglibProxyFactory生成List<Order>的代理对象
  4. 将代理对象设置到user.orders属性上

此时,user对象已经返回,但user.orders是一个代理对象,还没有执行订单查询。

步骤 3:调用 getter 方法触发加载

// 此时才会触发订单查询
List<Order> orders = user.getOrders();

代理对象的intercept方法被调用:

  1. 检查orders是否已经加载,发现没有加载
  2. 调用ResultLoader.loadResult()方法
  3. 从 SqlSession 中获取OrderMapper,执行getOrdersByUserId(1L)
  4. 执行订单查询 SQL:
    SELECT * FROM order WHERE user_id = 1
  5. 将查询得到的真实List<Order>对象替换代理对象
  6. 返回真实的订单列表

步骤 4:后续调用直接返回真实对象

// 第二次调用,直接返回真实对象,不会再执行SQL
List<Order> orders2 = user.getOrders();

此时,orders已经加载完成,后续调用会直接返回真实对象,不会再执行 SQL 查询。

五、两种延迟加载模式

根据aggressiveLazyLoading参数的不同,MyBatis 有两种延迟加载模式:

1. 侵入式延迟加载(aggressiveLazyLoading = true)

当调用主对象的任何方法时,都会触发所有延迟加载属性的加载。

示例

User user = userMapper.getUserById(1L);
// 调用toString()方法,会触发orders和dept两个延迟加载属性的加载
System.out.println(user.toString());

这种模式的缺点很明显:即使我们不需要关联对象,只要调用了主对象的任何方法,都会触发所有关联查询,失去了延迟加载的意义。

2. 按需延迟加载(aggressiveLazyLoading = false)

只有调用对应延迟加载属性的 getter 方法时,才会触发该属性的加载。

示例

User user = userMapper.getUserById(1L);
// 调用toString()方法,不会触发任何延迟加载
System.out.println(user.toString());
// 只有调用getOrders()时,才会触发订单查询
List<Order> orders = user.getOrders();
// 调用getDept()时,才会触发部门查询
Dept dept = user.getDept();

这是推荐的模式,也是 MyBatis 3.4.1 及以后的默认模式。它真正实现了按需加载,只有在需要的时候才会执行查询。

六、常见坑点与避坑指南

1. 坑 1:嵌套结果不支持延迟加载

问题:很多人以为所有关联查询都支持延迟加载,其实只有嵌套查询(select属性)支持,嵌套结果不支持。

错误示例

<!-- 嵌套结果,不支持延迟加载,会一次性查询所有数据 -->
<resultMap id="userResultMap" type="User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <collection property="orders" ofType="Order">
        <id column="order_id" property="id"/>
        <result column="order_no" property="orderNo"/>
    </collection>
</resultMap>
<select id="getUserById" resultMap="userResultMap">
    SELECT u.*, o.id as order_id, o.order_no 
    FROM user u LEFT JOIN order o ON u.id = o.user_id 
    WHERE u.id = #{id}
</select>

原因:嵌套结果是通过多表联查一次性获取所有数据,然后在内存中进行结果映射,无法实现按需加载。

2. 坑 2:Session 关闭后访问延迟加载属性

问题:如果在 SqlSession 关闭后访问延迟加载的属性,会抛出LazyInitializationException异常。

错误示例

SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1L);
sqlSession.close(); // 关闭SqlSession
// 此时访问延迟加载属性,会抛出异常
List<Order> orders = user.getOrders();

原因:延迟加载需要使用 SqlSession 来执行查询。如果 SqlSession 已经关闭,就无法执行 SQL 查询了。

解决方案

  • 在 SqlSession 关闭前访问所有需要的延迟加载属性
  • 使用 Spring 整合 MyBatis,Spring 会自动管理 SqlSession 的生命周期,不会出现这个问题
  • 关闭延迟加载,使用立即加载

3. 坑 3:实体类是 final 的

问题:如果实体类是 final 的,CGLIB 无法生成代理对象,导致延迟加载失效。

错误示例

// final类,无法被CGLIB代理
public final class User {
    private Long id;
    private String name;
    private List<Order> orders;
    // getter和setter
}

原因:CGLIB 动态代理是基于继承实现的,无法继承 final 类。

解决方案:去掉实体类的 final 修饰符。

4. 坑 4:在 equals、hashCode、toString 中访问延迟加载属性

问题:如果在实体类的equals()hashCode()toString()方法中访问了延迟加载的属性,会触发不必要的查询。

错误示例

public class User {
    private Long id;
    private String name;
    private List<Order> orders;
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", orders=" + orders + // 访问延迟加载属性
                '}';
    }
}

原因:当调用user.toString()时,会访问orders属性,触发延迟加载,执行不必要的 SQL 查询。

解决方案:在equals()hashCode()toString()方法中,只包含基本属性,不要包含延迟加载的关联属性。

5. 坑 5:N+1 查询问题

问题:延迟加载本质上就是 N+1 查询问题。如果查询了 100 个用户,然后每个用户都调用getOrders(),会执行 1+100=101 次 SQL 查询。

原因:先执行 1 次主查询获取所有用户,然后每个用户执行 1 次订单查询。

解决方案

  • 如果需要访问所有用户的订单,使用嵌套结果的一次性查询
  • 如果只有少数用户需要访问订单,使用延迟加载
  • 使用批量查询优化,减少 SQL 查询次数

七、最佳实践

  1. 合理使用延迟加载:只在关联对象不常访问的场景下使用延迟加载。如果经常访问关联对象,使用嵌套结果的一次性查询性能更好。
  2. 关闭侵入式延迟加载:将aggressiveLazyLoading设置为false,实现真正的按需加载。
  3. 优先使用局部配置:不要全局开启延迟加载,而是在需要的关联查询上单独使用fetchType="lazy"配置,避免不必要的延迟加载。
  4. 避免在 Session 关闭后访问延迟加载属性:在 Spring 整合 MyBatis 的环境中,这个问题会自动解决;在原生 MyBatis 环境中,要确保在 Session 关闭前访问所有需要的属性。
  5. 实体类不要加 final 修饰符:避免 CGLIB 无法生成代理对象。
  6. 不要在 equals、hashCode、toString 中访问延迟加载属性:避免触发不必要的查询。
  7. 监控 SQL 执行:在开发环境中开启 SQL 日志,检查是否有不必要的延迟加载查询。

八、高频面试题解答

  • 问:MyBatis 支持延迟加载吗?原理是什么? 答:MyBatis 支持延迟加载,但只支持嵌套查询的延迟加载。它的底层原理是动态代理:当发现某个关联属性需要延迟加载时,MyBatis 会为该属性生成一个代理对象。当调用代理对象的 getter 方法时,才会触发真正的 SQL 查询,加载真实数据。
  • 问:lazyLoadingEnabledaggressiveLazyLoading有什么区别? 答:lazyLoadingEnabled是全局延迟加载开关,控制是否开启延迟加载;aggressiveLazyLoading是侵入式延迟加载开关,控制何时触发延迟加载。当aggressiveLazyLoading为 true 时,调用主对象的任何方法都会触发所有延迟加载属性的加载;为 false 时,只有调用对应属性的 getter 方法才会触发加载。
  • 问:为什么嵌套结果不支持延迟加载? 答:嵌套结果是通过多表联查一次性获取所有数据,然后在内存中进行结果映射。它在查询时已经获取了所有关联数据,无法实现按需加载。只有嵌套查询是先查询主表,关联数据在需要时再单独查询,所以支持延迟加载。
  • 问:MyBatis 使用什么动态代理实现延迟加载?为什么? 答:MyBatis 默认使用 CGLIB 动态代理,也支持 Javassist 动态代理。因为 JDK 动态代理只能代理接口,而我们要代理的是实体类,所以不能使用 JDK 动态代理。CGLIB 和 Javassist 都是基于字节码生成的动态代理,可以代理普通类。
  • 问:延迟加载有什么优缺点? 答:优点是按需加载,减少不必要的数据库查询,降低内存占用,提高系统性能;缺点是会产生 N+1 查询问题,并且在 Session 关闭后无法访问延迟加载属性。
  • 问:延迟加载的对象在 Session 关闭后为什么不能使用? 答:因为延迟加载需要使用 SqlSession 来执行 SQL 查询。如果 SqlSession 已经关闭,就无法获取数据库连接,也就无法执行查询了,会抛出LazyInitializationException异常。
  • 问:如何解决延迟加载的 N+1 问题? 答:如果需要访问所有关联对象,使用嵌套结果的一次性查询;如果只有少数对象需要访问关联对象,使用延迟加载;也可以使用批量查询优化,减少 SQL 查询次数。

九、总结

MyBatis 延迟加载是一个非常实用的性能优化特性,它通过动态代理实现了按需加载,减少了不必要的数据库查询和内存占用。

回顾一下全文的核心内容:

  • MyBatis 只支持嵌套查询的延迟加载,不支持嵌套结果的延迟加载
  • 延迟加载的底层原理是动态代理,默认使用 CGLIB 实现
  • 两个核心参数:lazyLoadingEnabled控制是否开启延迟加载,aggressiveLazyLoading控制何时触发加载
  • 延迟加载会产生 N+1 查询问题,需要根据场景合理使用
  • 常见坑点包括 Session 关闭后访问、实体类是 final 的、在 toString 中访问延迟加载属性等

理解了延迟加载的原理和坑点,你就能在实际项目中正确使用延迟加载,提高系统性能,同时避免常见的问题。

到此这篇关于MyBatis 延迟加载深度解密:从使用方式到底层动态代理原理解析的文章就介绍到这了,更多相关MyBatis 延迟加载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • MyBatis获取参数值的两种方式详解

    MyBatis获取参数值的两种方式详解

    本文主要介绍了MyBatis获取参数值的两种方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • java封装的概念和实现方法示例

    java封装的概念和实现方法示例

    这篇文章主要介绍了java封装的概念和实现方法,结合实例形式详细分析了java封装的概念、原理及相关使用技巧,需要的朋友可以参考下
    2019-11-11
  • Maven 不同环境灵活构建的步骤

    Maven 不同环境灵活构建的步骤

    在项目开发过程中,合理地使用Maven管理不同的构建环境(开发、测试、生产)是提高项目管理效率和应对复杂项目需求的关键,本文就来介绍一下Maven 不同环境灵活构建的步骤,感兴趣的可以了解一下
    2024-10-10
  • Java8 HashMap的实现原理分析

    Java8 HashMap的实现原理分析

    Java8之后新增挺多新东西,接下来通过本文给大家介绍Java8 HashMap的实现原理分析,对java8 hashmap实现原理相关知识感兴趣的朋友一起学习吧
    2016-03-03
  • java中double强制转换int引发的OOM问题记录

    java中double强制转换int引发的OOM问题记录

    这篇文章主要介绍了java中double强制转换int引发的OOM问题记录,本文给大家分享问题排查过程,感兴趣的朋友跟随小编一起看看吧
    2024-10-10
  • Druid数据库连接池监控使用及说明

    Druid数据库连接池监控使用及说明

    Druid是Java数据库连接池,具有监控和扩展功能,性能好,自带监控页面,支持密码加密、SQL执行日志等,Druid已经在阿里巴巴部署了超过600个应用
    2025-12-12
  • Java如何通过SSE实现消息推送详解

    Java如何通过SSE实现消息推送详解

    这篇文章主要介绍了Java如何通过SSE实现消息推送的相关资料,SSE是一种服务器向客户端推送数据的技术,基于HTTP协议,利用长连接特性,它适用于单向数据流场景,如股票价格更新、新闻实时推送等,需要的朋友可以参考下
    2025-04-04
  • java控制台实现学生信息管理系统(IO版)

    java控制台实现学生信息管理系统(IO版)

    这篇文章主要为大家详细介绍了java控制台实现学生信息管理系统(IO版),文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04
  • Spring中Properties的配置方式

    Spring中Properties的配置方式

    这篇文章主要介绍了Spring中Properties的配置方式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • Map集合之HashMap的使用及说明

    Map集合之HashMap的使用及说明

    这篇文章主要介绍了Map集合之HashMap的使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10

最新评论