一、前言

在 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&amp;characterEncoding=utf8&amp;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>
关键参数说明
配置项取值作用
lazyLoadingEnabledtrue开启全局延迟加载(基础开关)
aggressiveLazyLoadingfalse关闭 “激进加载”,保证仅加载当前使用的属性(实现真正的按需加载)

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 适用场景

  1. 关联数据不常用:如查询用户列表时,仅少数场景需要用户的订单信息;
  2. 大数据量查询:避免一次性加载大量关联数据导致内存溢出;
  3. 性能优化:减少不必要的数据库交互,降低 IO 开销。

5.2 注意事项

  1. N+1 查询问题:若查询多个用户并触发每个用户的订单懒加载,会导致 1 次用户查询 + N 次订单查询,需结合fetchType="eager"或批量查询优化;
  2. 局部配置优先级更高fetchType="eager"会覆盖全局的lazyLoadingEnabled=true
  3. 序列化问题:懒加载对象被序列化(如返回给前端)时,会触发所有关联数据加载,需手动排除不需要的属性。

六、总结

关键点回顾

  1. 延迟加载的核心是「按需加载」:用到关联数据才触发查询,核心依赖分布式分段查询;
  2. 核心配置:全局开启lazyLoadingEnabled=true + 关闭aggressiveLazyLoading=false,局部通过fetchType="lazy"控制;
  3. 实战关键:通过<association>(一对一)/<collection>(一对多)的select属性指定子查询,fetchType指定加载策略;
  4. 核心价值:减少冗余查询,提升 MyBatis 查询性能,但需规避 N+1 问题。

扩展说明

延迟加载的配置逻辑可直接复用至「一对多」「多对多」场景:

  • 一对多:将<association>替换为<collection>,其余配置完全一致;
  • 多对多:新增中间表,通过两次分段查询实现懒加载。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐