MyBatis 延迟加载(懒加载)解析笔记
·
一、前言
在 MyBatis 关联查询中,「分布式分段查询」并非单纯为了拆分 SQL,其核心目的是为了配合 MyBatis 的延迟加载(懒加载)策略,实现「按需加载数据」。延迟加载能避免查询冗余数据,减少数据库交互开销,是优化 MyBatis 查询性能的关键手段。本文以「订单 - 用户一对一关联」为例,从核心概念、配置方式到实战演示,完整讲解 MyBatis 延迟加载的实现逻辑与最佳实践。
二、延迟加载核心概念:什么是 “按需加载”?
2.1 基础定义
延迟加载(也叫懒加载 / 按需加载):指在查询关联数据时,不立即加载所有关联信息,而是等到程序真正需要使用该关联数据时,才触发对应的 SQL 查询。
2.2 核心思想
- 立即加载:查询主数据时,一次性加载所有关联数据(哪怕用不到);
- 延迟加载:只加载当前业务需要的核心数据,关联数据 “用到才查,不用不查”。
2.3 典型场景理解
以「用户 - 订单」为例:
- 若业务只需要获取用户的
username,则无需加载订单表数据; - 若业务需要打印用户 + 订单的完整信息,则再触发订单表的查询;
- 核心价值:避免无意义的关联查询,减少数据库 IO。
三、MyBatis 延迟加载核心配置
延迟加载的控制分为「全局配置」和「局部配置」,全局配置决定整体开关,局部配置可针对单个查询精准控制。
3.1 全局配置(mybatis-config.xml)
全局配置是延迟加载的基础,需在settings标签中配置,核心参数有两个
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局设置 -->
<settings>
<!-- 1. 延迟加载全局开关:true=开启,false=关闭(默认false) -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 2. 激进懒加载:控制是否加载对象所有属性(关键!) -->
<!-- true:调用对象任意方法都会加载所有关联属性(无懒加载效果) -->
<!-- false:仅加载当前需要的属性(真正的按需加载,推荐) -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 可选:指定懒加载时使用的代理类型(默认即可) -->
<setting name="proxyFactory" value="CGLIB"/>
</settings>
<!-- 环境配置、映射器配置等 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/OrderMapper.xml"/>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
关键参数说明
| 配置项 | 取值 | 作用 |
|---|---|---|
lazyLoadingEnabled | true | 开启全局延迟加载(基础开关) |
aggressiveLazyLoading | false | 关闭 “激进加载”,保证仅加载当前使用的属性(实现真正的按需加载) |
3.2 局部配置(Mapper.xml)
全局开关开启后,可通过fetchType属性控制单个关联查询的加载策略,优先级高于全局配置:
fetchType="lazy":当前关联查询使用懒加载(按需);fetchType="eager":当前关联查询强制立即加载(无视全局配置)。
四、一对一场景:延迟加载实战(订单 - 用户)
4.1 前置准备(复用之前的基础结构)
4.1.1 数据库表(用户表 / 订单表)
-- 用户表
CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单表
CREATE TABLE `order` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` VARCHAR(30) NOT NULL COMMENT '订单编号',
`amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
`user_id` BIGINT NOT NULL COMMENT '关联用户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
CONSTRAINT `fk_order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.1.2 实体类
// 用户实体类
public class User {
private Long id; // 用户ID
private String username; // 用户名
private String phone; // 手机号
private Date createTime; // 创建时间
// 一对一关联:用户对应的订单(业务场景限定一对一)
private Order order;
// 省略getter/setter/toString方法
}
// 订单实体类
public class Order {
private Long id; // 订单ID
private String orderNo; // 订单编号
private BigDecimal amount; // 订单金额
private Long userId; // 关联用户ID
private Date createTime; // 创建时间
// 省略getter/setter/toString方法
}
4.2 Mapper 层:分布式分段查询 + 延迟加载配置
4.2.1 UserMapper 接口 & XML(查询用户 + 关联订单)
// UserMapper接口
public interface UserMapper {
/**
* 查询用户,并懒加载其关联的订单
* @param userId 用户ID
* @return 包含懒加载订单的User对象
*/
User selectUserWithLazyOrder(Long userId);
}
<!-- UserMapper.xml -->
<?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.example.mapper.UserMapper">
<!-- 定义ResultMap:用户+懒加载订单 -->
<resultMap id="UserLazyOrderResultMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="phone" property="phone"/>
<result column="create_time" property="createTime"/>
<!-- 一对一关联订单:使用懒加载 -->
<association
property="order" <!-- User类中的order属性 -->
javaType="com.example.entity.Order" <!-- 关联实体类型 -->
column="id" <!-- 关联字段:用户ID(传给子查询) -->
select="com.example.mapper.OrderMapper.selectOrderByUserId" <!-- 子查询方法 -->
fetchType="lazy"/> <!-- 局部懒加载配置(核心) -->
</resultMap>
<!-- 查询用户主数据 -->
<select id="selectUserWithLazyOrder" resultMap="UserLazyOrderResultMap">
SELECT id, username, phone, create_time FROM `user` WHERE id = #{userId}
</select>
</mapper>
4.2.2 OrderMapper 接口 & XML(子查询:根据用户 ID 查订单)
// OrderMapper接口
public interface OrderMapper {
/**
* 根据用户ID查询订单(子查询,供懒加载调用)
* @param userId 用户ID
* @return 订单信息
*/
Order selectOrderByUserId(Long userId);
}
<!-- OrderMapper.xml -->
<?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.example.mapper.OrderMapper">
<select id="selectOrderByUserId" resultType="com.example.entity.Order">
SELECT id, order_no, amount, user_id, create_time
FROM `order`
WHERE user_id = #{userId}
</select>
</mapper>
4.3 测试:延迟加载效果演示
编写测试类,对比 “仅用用户字段” 和 “使用订单字段” 的 SQL 执行情况:
public class LazyLoadingTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
// 加载MyBatis配置文件,创建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testLazyLoading() {
try (SqlSession session = sqlSessionFactory.openSession(true)) {
UserMapper userMapper = session.getMapper(UserMapper.class);
// 第一步:查询用户(此时仅执行用户表SQL,不执行订单表SQL)
User user = userMapper.selectUserWithLazyOrder(1L);
System.out.println("=== 仅获取用户名 ===");
// 只使用用户的username属性,不触发订单查询
System.out.println("用户名:" + user.getUsername());
System.out.println("\n=== 获取订单信息 ===");
// 使用订单属性,触发懒加载:执行OrderMapper的selectOrderByUserId
Order order = user.getOrder();
System.out.println("订单编号:" + order.getOrderNo());
System.out.println("订单金额:" + order.getAmount());
}
}
}
4.4 效果验证
4.4.1 仅获取用户名时的 SQL 日志
==> Preparing: SELECT id, username, phone, create_time FROM `user` WHERE id = ?
==> Parameters: 1(Long)
<== Columns: id, username, phone, create_time
<== Row: 1, 张三, 13800138000, 2026-02-12 10:00:00
<== Total: 1
=== 仅获取用户名 ===
用户名:张三
结论:仅执行用户表查询,订单表 SQL 未触发。
4.4.2 获取订单信息时的 SQL 日志
=== 获取订单信息 ===
==> Preparing: SELECT id, order_no, amount, user_id, create_time FROM `order` WHERE user_id = ?
==> Parameters: 1(Long)
<== Columns: id, order_no, amount, user_id, create_time
<== Row: 1, ORDER20260212001, 99.00, 1, 2026-02-12 10:05:00
<== Total: 1
订单编号:ORDER20260212001
订单金额:99.00
结论:使用订单属性时,才触发订单表的查询(真正的按需加载)。
4.5 反例:aggressiveLazyLoading=true 的效果
若将aggressiveLazyLoading设为true,即使只调用user.getUsername(),也会触发订单表查询:
==> Preparing: SELECT id, username, phone, create_time FROM `user` WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
==> Preparing: SELECT id, order_no, amount, user_id, create_time FROM `order` WHERE user_id = ?
==> Parameters: 1(Long)
<== Total: 1
=== 仅获取用户名 ===
用户名:张三
结论:激进加载会破坏懒加载效果,务必设为false。
五、延迟加载的使用场景与注意事项
5.1 适用场景
- 关联数据不常用:如查询用户列表时,仅少数场景需要用户的订单信息;
- 大数据量查询:避免一次性加载大量关联数据导致内存溢出;
- 性能优化:减少不必要的数据库交互,降低 IO 开销。
5.2 注意事项
- N+1 查询问题:若查询多个用户并触发每个用户的订单懒加载,会导致 1 次用户查询 + N 次订单查询,需结合
fetchType="eager"或批量查询优化; - 局部配置优先级更高:
fetchType="eager"会覆盖全局的lazyLoadingEnabled=true; - 序列化问题:懒加载对象被序列化(如返回给前端)时,会触发所有关联数据加载,需手动排除不需要的属性。
六、总结
关键点回顾
- 延迟加载的核心是「按需加载」:用到关联数据才触发查询,核心依赖分布式分段查询;
- 核心配置:全局开启
lazyLoadingEnabled=true+ 关闭aggressiveLazyLoading=false,局部通过fetchType="lazy"控制; - 实战关键:通过
<association>(一对一)/<collection>(一对多)的select属性指定子查询,fetchType指定加载策略; - 核心价值:减少冗余查询,提升 MyBatis 查询性能,但需规避 N+1 问题。
扩展说明
延迟加载的配置逻辑可直接复用至「一对多」「多对多」场景:
- 一对多:将
<association>替换为<collection>,其余配置完全一致; - 多对多:新增中间表,通过两次分段查询实现懒加载。
更多推荐



所有评论(0)