乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

延迟加载

导学

本节学习目标:

  • 理解延迟加载解决的核心痛点:避免不必要的数据库查询,降低内存和网络开销
  • 掌握全局开关 lazyLoadingEnabled 与局部覆盖 fetchType 的配合使用
  • 理解 aggressiveLazyLoading 激进延迟加载的行为差异
  • 了解 lazyLoadTriggerMethods 触发方法的配置与影响
  • 理解 CGLIB 代理的实现原理,明确延迟加载的生效边界
  • 能够通过日志对比立即加载与延迟加载的 SQL 执行时序差异

定义

延迟加载(Lazy Loading)是 MyBatis 提供的一种按需查询机制。当开启延迟加载后,MyBatis 在首次查询主对象时不立即执行关联对象(association 或 collection)的子查询,而是返回一个 CGLIB 生成的代理对象。只有当代码真正访问关联属性(如调用 getMentor() 或遍历 getStudents())时,代理对象才会触发实际的 SQL 子查询,从数据库加载关联数据。

它解决的核心痛点:

  • 关联对象并非每次都被使用,立即加载会造成不必要的数据库往返和内存占用
  • 在复杂对象图中,级联加载可能导致一次性查询大量数据,拖慢主查询响应时间
  • 允许开发者以统一的方式编写映射配置,由运行时行为决定实际加载时机

适用位置与核心属性

延迟加载的配置分为全局配置和局部配置两个层面。

全局配置(mybatis-config.xml)

属性默认值说明
lazyLoadingEnabledfalse全局延迟加载开关。设为 true 后,所有嵌套 Select 方式的 association 和 collection 默认延迟加载
aggressiveLazyLoadingfalse(3.4.1 之前为 true)激进延迟加载。设为 true 时,访问主对象的任意方法都会触发所有未加载的关联属性加载
lazyLoadTriggerMethodsequals,clone,hashCode,toString指定哪些方法被调用时会触发延迟加载。默认情况下,调用 toString() 等方法会强制加载所有未初始化的关联属性

局部配置(映射文件)

属性适用元素说明
fetchType<association>、<collection>局部覆盖全局配置:lazy 表示延迟加载,eager 表示立即加载。仅在嵌套 Select 方式下有效

配置示例(mybatis-config.xml):

<settings>
    <!-- 开启全局延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭激进延迟加载:只有真正访问关联属性时才加载 -->
    <setting name="aggressiveLazyLoading" value="false"/>
    <!-- 自定义触发方法:去掉toString,避免调试时意外触发 -->
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode"/>
</settings>

核心原理

延迟加载触发时序图(CGLIB 代理机制)

MyBatis 的延迟加载依赖 CGLIB 字节码生成技术。当查询返回的主对象包含延迟加载的关联属性时,MyBatis 不会直接注入真实对象,而是注入一个 CGLIB 代理。代理内部持有子查询的上下文信息(SQL ID、参数值、配置对象),直到触发条件满足时才执行查询。

关键机制说明:

  1. 代理创建时机:MyBatis 在创建主对象后,对标记为延迟加载的关联属性字段,注入 CGLIB 代理实例而非真实对象。
  2. 触发判断:代理对象拦截所有方法调用。当检测到是“获取关联属性”的方法(如 getter)时,检查目标对象是否已加载。
  3. 子查询执行:若未加载,代理对象通过 Executor 发起子查询,参数值来自主查询时 column 指定的列值。
  4. 结果缓存:首次加载后,真实对象被缓存到主对象内部,后续访问直接返回,不再重复查询。
  5. 作用域限制:延迟加载的代理对象绑定到创建它的 SqlSession。如果 SqlSession 已关闭,再次访问代理属性会抛出 LazyInitializationException(类似 Hibernate 的懒加载异常)。

完整示例

场景说明

乐途公司学生管理系统中,查询学生列表时通常只需要学生基本信息,导师详情仅在查看学生详情页时才需要。我们将对比:

  • 立即加载:查询学生时自动加载导师
  • 延迟加载:查询学生时不加载导师,直到调用 getMentor()

操作前的数据库表结构及初始数据

沿用统一表结构:

-- 学生表
CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2),
    class_id INT,
    mentor_id INT
);

-- 导师表
CREATE TABLE mentor (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    title VARCHAR(20)
);

初始数据:

idnameagemajorscoreclass_idmentor_id
1大翔22软件工程89.5011
2白歌21计算机科学92.0012
idnametitle
1黄俪副教授
2李眉教授

MyBatis 全局配置

<!-- mybatis-config.xml -->
<configuration>
    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode"/>
    </settings>
    <!-- 其他配置省略... -->
</configuration>

完整的映射文件片段

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.flywing.mapper.StudentMapper">

    <resultMap id="MentorMap" type="com.flywing.entity.Mentor">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="title" column="title"/>
    </resultMap>

    <resultMap id="StudentMap" type="com.flywing.entity.Student">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="major" column="major"/>
        <result property="score" column="score"/>
        <result property="classId" column="class_id"/>
        <result property="mentorId" column="mentor_id"/>
        <!-- 默认跟随全局延迟加载策略 -->
        <association property="mentor" javaType="com.flywing.entity.Mentor"
                     column="mentor_id"
                     select="selectMentorById"/>
    </resultMap>

    <!-- 局部强制立即加载的resultMap(用于对比) -->
    <resultMap id="StudentEagerMap" type="com.flywing.entity.Student"
               extends="StudentMap">
        <association property="mentor" javaType="com.flywing.entity.Mentor"
                     column="mentor_id"
                     select="selectMentorById"
                     fetchType="eager"/>
    </resultMap>

    <select id="selectStudentsLazy" resultMap="StudentMap">
        SELECT id, name, age, major, score, class_id, mentor_id
        FROM student
    </select>

    <select id="selectStudentsEager" resultMap="StudentEagerMap">
        SELECT id, name, age, major, score, class_id, mentor_id
        FROM student
    </select>

    <select id="selectMentorById" resultMap="MentorMap">
        SELECT id, name, title FROM mentor WHERE id = #{id}
    </select>

</mapper>

实际执行结果

测试代码(延迟加载场景):

@Test
public void testLazyLoading() {
    List<Student> students = mapper.selectStudentsLazy();
    System.out.println("=== 主查询完成,尚未访问关联属性 ===");
    for (Student s : students) {
        System.out.println("学生: " + s.getName());
    }
    System.out.println("=== 开始访问导师属性 ===");
    for (Student s : students) {
        Mentor mentor = s.getMentor(); // 触发延迟加载
        System.out.println("学生: " + s.getName() + ", 导师: " + mentor.getName());
    }
}

延迟加载控制台 SQL 输出:

==>  Preparing: SELECT id, name, age, major, score, class_id, mentor_id FROM student
==>  Parameters:
<==  Columns: id, name, age, major, score, class_id, mentor_id
<==  Row: 1, 大翔, 22, 软件工程, 89.50, 1, 1
<==  Row: 2, 白歌, 21, 计算机科学, 92.00, 1, 2
<==  Total: 2
=== 主查询完成,尚未访问关联属性 ===
学生: 大翔
学生: 白歌
=== 开始访问导师属性 ===
==>  Preparing: SELECT id, name, title FROM mentor WHERE id = ?
==>  Parameters: 1(Integer)
<==  Columns: id, name, title
<==  Row: 1, 黄俪, 副教授
学生: 大翔, 导师: 黄俪
==>  Preparing: SELECT id, name, title FROM mentor WHERE id = ?
==>  Parameters: 2(Integer)
<==  Columns: id, name, title
<==  Row: 2, 李眉, 教授
学生: 白歌, 导师: 李眉

立即加载控制台 SQL 输出(selectStudentsEager):

==>  Preparing: SELECT id, name, age, major, score, class_id, mentor_id FROM student
==>  Parameters:
<==  Columns: id, name, age, major, score, class_id, mentor_id
<==  Row: 1, 大翔, 22, 软件工程, 89.50, 1, 1
====>  Preparing: SELECT id, name, title FROM mentor WHERE id = ?
====>  Parameters: 1(Integer)
<====  Columns: id, name, title
<====  Row: 1, 黄俪, 副教授
<==  Row: 2, 白歌, 21, 计算机科学, 92.00, 1, 2
====>  Preparing: SELECT id, name, title FROM mentor WHERE id = ?
====>  Parameters: 2(Integer)
<====  Columns: id, name, title
<====  Row: 2, 李眉, 教授
<==  Total: 2

分析

  • 延迟加载时序:主查询完成后,控制台仅输出 1 条 SQL。直到代码调用 getMentor(),才逐条触发导师查询。如果循环中从未访问 mentor 属性,则导师查询一次都不会执行。
  • 立即加载时序:主查询的每一行都会立即触发子查询,与 association 嵌套 Select 的默认行为一致。
  • fetchType 局部覆盖:StudentEagerMap 中 fetchType="eager" 覆盖了全局 lazyLoadingEnabled=true,使该查询强制立即加载。这种灵活性允许在全局懒加载策略下,对关键关联做例外处理。
  • 代理对象限制:延迟加载返回的 Student 对象实际上是 CGLIB 代理的子类实例。如果尝试在 SqlSession 关闭后访问 getMentor(),会抛出异常。因此延迟加载对象必须在同一会话内使用,或提前加载所需数据。

易错场景 / 常见误区

误区正解
认为延迟加载对嵌套结果映射(JOIN 方式)也有效延迟加载仅在嵌套 Select 方式下生效。JOIN 查询已一次性返回全部数据,不存在后续加载时机
开启 aggressiveLazyLoading=true 后,以为只有访问关联属性才加载激进模式下,访问主对象的任意方法(包括 getName())都会触发所有未加载关联属性的查询,失去按需加载的意义
调试时调用 System.out.println(student) 发现关联属性被意外加载默认 lazyLoadTriggerMethods 包含 toString(),打印对象会触发延迟加载。如需避免,应从触发方法列表中移除 toString
SqlSession 关闭后,尝试访问延迟加载的关联属性代理对象依赖 SqlSession 的 Executor 执行子查询。会话关闭后访问会抛异常。应在关闭前完成数据加载,或使用 EAGER 策略
认为 fetchType="lazy" 可以覆盖 aggressiveLazyLoading 的行为fetchType 只控制加载时机(立即/延迟),aggressiveLazyLoading 控制延迟加载的触发范围。二者是正交配置

面试考点

Q1:MyBatis 延迟加载的实现原理是什么?为什么返回的对象是 CGLIB 代理而不是原始类型?

A:MyBatis 使用 CGLIB 生成主对象和关联对象的代理子类。代理对象在字段级别拦截访问,当检测到关联属性未初始化时,通过 Executor 发起子查询,加载真实数据后替换代理。使用代理而非原始类型的原因是:Java 无法在运行时动态替换对象引用,只有通过代理拦截方法调用,才能在“访问时”插入加载逻辑。

Q2:lazyLoadingEnabled=true 且 aggressiveLazyLoading=true 时,调用主对象的 getName() 会触发关联加载吗?为什么?

A:会。aggressiveLazyLoading 为 true 时,MyBatis 认为任何方法调用都可能需要完整对象状态,因此会一次性加载所有未初始化的关联属性。这在 3.4.1 之前是默认行为,但通常会导致性能劣化,现代项目建议关闭。

Q3:延迟加载的代理对象在 SqlSession 关闭后还能使用吗?与 Hibernate 的懒加载异常有什么异同?

A:不能。MyBatis 的延迟加载代理依赖创建它的 SqlSession 及其 Executor 来执行子查询。会话关闭后再次访问未加载的关联属性,会抛出类似 LazyInitializationException 的异常(具体异常信息取决于 MyBatis 版本)。这与 Hibernate 的 LazyInitializationException 本质相同:都是会话作用域外的代理访问问题。解决方案也类似:在会话内完成加载、使用 Open Session in View 模式、或改为 EAGER 加载。

Q4:生产环境中,如何配置延迟加载才能既享受性能收益,又避免调试和日志中的意外触发?

A:推荐配置组合:(1)lazyLoadingEnabled=true 开启全局延迟加载;(2)aggressiveLazyLoading=false 避免任意方法触发;(3)lazyLoadTriggerMethods=equals,clone,hashCode 移除 toString,防止日志和调试打印时意外加载;(4)对必须立即加载的关键关联,使用 fetchType="eager" 局部覆盖。

小结

延迟加载是 MyBatis 优化关联查询性能的重要武器。通过 CGLIB 代理机制,框架将关联对象的加载时机推迟到真正访问时,避免了不必要的数据库往返。全局配置 lazyLoadingEnabled 与局部 fetchType 的配合,提供了灵活的策略控制;而 aggressiveLazyLoading 和 lazyLoadTriggerMethods 则决定了延迟加载的触发边界。理解代理对象的生命周期限制(绑定 SqlSession),是避免运行时异常的关键。

下一章引子

延迟加载缓解了关联查询的性能压力,但它只是“推迟”了子查询的执行,并未减少总查询次数。如果业务场景需要一次性批量加载关联数据,嵌套 Select 方式固有的 N+1 查询问题 仍然是性能杀手。下一节将系统剖析 N+1 问题的成因、检测手段和根治方案。

上一页
N+1查询问题