MyBatis 高级映射与延迟加载完全指南

MyBatis 高级映射与延迟加载完全指南

一、概述

在实际企业级开发中,数据库表之间往往存在复杂的关联关系(一对一、一对多)。MyBatis 作为优秀的持久层框架,通过 resultMap 的高级映射 功能,提供了优雅的解决方案来处理这些关联关系。

同时,为了优化性能,MyBatis 引入了 延迟加载(Lazy Loading) 机制,允许我们在真正需要关联数据时才去查询数据库,避免不必要的性能开销。

本文将深入剖析 MyBatis 的高级映射(associationcollection)及延迟加载的配置与使用,并结合 N+1 查询问题给出最佳实践。

二、高级映射:关联关系处理

在 MyBatis 中,处理关联关系主要依赖 <resultMap> 的两个子标签:

  • <association> :处理 一对一(One-to-One) 关系。
  • <collection> :处理 一对多(One-to-Many) 关系。

这两种关联映射都支持两种不同的实现方式:

  1. 嵌套结果(Nested Results) :通过一次 JOIN 查询,将主表和关联表的数据一次性查出来,然后进行映射(性能最优)。
  2. 嵌套查询(Nested Queries):先查主表,然后通过另一条 SQL 语句去查关联表(配合延迟加载,实现按需查询)。

2.1 一对一映射(association

场景:每辆车(Car)都有一个发动机(Engine)。

实体类
java 复制代码
public class Car {
    private Long id;
    private String carNum;
    private String brand;
    private Engine engine;  // 一对一关联
    // getter/setter...
}

public class Engine {
    private Long id;
    private String type;    // V8, 电动
    private Integer power;  // 功率
}
方式一:嵌套结果(JOIN 查询,推荐)

原理:一次 SQL 搞定,性能最高。

xml 复制代码
<resultMap id="CarEngineMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- 一对一:映射关联的 Engine 对象 -->
    <association property="engine" javaType="Engine">
        <id property="id" column="engine_id"/>
        <result property="type" column="engine_type"/>
        <result property="power" column="engine_power"/>
    </association>
</resultMap>

<select id="selectCarWithEngine" resultMap="CarEngineMap">
    SELECT 
        c.id, c.car_num, c.brand,
        e.id AS engine_id, 
        e.type AS engine_type, 
        e.power AS engine_power
    FROM t_car c
    LEFT JOIN t_engine e ON c.id = e.car_id
    WHERE c.id = #{id}
</select>
方式二:嵌套查询(配合延迟加载)

原理 :先查 Car,再根据 car_id 单独查 Engine。

xml 复制代码
<resultMap id="CarEngineMapLazy" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- select:指向另一个 Mapper 的查询方法,column:传给子查询的列名 -->
    <association property="engine" 
                 javaType="Engine"
                 select="com.xie.mapper.EngineMapper.selectByCarId"
                 column="id"
                 fetchType="lazy"/>  <!-- lazy:延迟加载 -->
</resultMap>

<select id="selectCarLazy" resultMap="CarEngineMapLazy">
    SELECT id, car_num, brand FROM t_car WHERE id = #{id}
</select>

2.2 一对多映射(collection

场景:一辆车(Car)有多个配件(Part)。

实体类
java 复制代码
public class Car {
    private Long id;
    private String carNum;
    private String brand;
    private List<Part> parts;  // 一对多关联
}

public class Part {
    private Long id;
    private String name;
    private Double price;
}
方式一:嵌套结果(JOIN 查询,推荐)
xml 复制代码
<resultMap id="CarPartsMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- 一对多:ofType 指定集合中元素的类型 -->
    <collection property="parts" ofType="Part">
        <id property="id" column="part_id"/>
        <result property="name" column="part_name"/>
        <result property="price" column="part_price"/>
    </collection>
</resultMap>

<select id="selectCarWithParts" resultMap="CarPartsMap">
    SELECT 
        c.id, c.car_num, c.brand,
        p.id AS part_id, 
        p.name AS part_name, 
        p.price AS part_price
    FROM t_car c
    LEFT JOIN t_part p ON c.id = p.car_id
    WHERE c.id = #{id}
</select>

注意collection 必须使用 ofType 指定集合泛型类型,而不是 javaTypejavaType 默认就是 List)。

方式二:嵌套查询(配合延迟加载)
xml 复制代码
<resultMap id="CarPartsMapLazy" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <collection property="parts" 
                ofType="Part"
                select="com.xie.mapper.PartMapper.selectByCarId"
                column="id"
                fetchType="lazy"/>
</resultMap>

三、延迟加载(Lazy Loading)

3.1 什么是延迟加载?

延迟加载是指:当 MyBatis 查询主对象时,不会立即查询关联对象,只有当我们调用关联对象的 getter 方法(如 car.getEngine())时,MyBatis 才会触发关联查询

3.2 配置方式

全局配置(mybatis-config.xml
xml 复制代码
<settings>
    <!-- 开启延迟加载(默认 false) -->
    <setting name="lazyLoadingEnabled" value="true"/>
    
    <!-- 激进加载(默认 true):如果为 true,只要主对象被访问,所有关联对象都会被加载。
         设为 false 则只会在显式调用 getter 时加载,推荐关闭 -->
    <setting name="aggressiveLazyLoading" value="false"/>
    
    <!-- 触发延迟加载的方法列表,用逗号分隔 -->
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
局部配置(在 association / collection 中覆盖)

通过 fetchType 属性覆盖全局配置:

  • fetchType="eager":立即加载(不使用延迟)。
  • fetchType="lazy":延迟加载(优先级高于全局配置)。
xml 复制代码
<association property="engine" 
             select="...selectByCarId" 
             column="id" 
             fetchType="lazy"/>   <!-- 该关联使用延迟加载 -->

3.3 核心原理(面试重点)

  1. MyBatis 为返回的 Car 对象生成一个代理对象(通过 CGLIB 或 Javassist)。
  2. 当你调用 car.getEngine() 时,代理对象会拦截该调用。
  3. 它判断 engine 属性是否为 null 且未加载,如果是,则执行预先配置好的 select SQL,将查询结果赋值给该属性。
  4. 返回真实的结果。

四、N+1 查询问题与性能陷阱

4.1 什么是 N+1 问题?

当使用嵌套查询 时,如果主查询返回了 N 条记录,那么为了加载每条记录的关联对象,MyBatis 会额外执行 N 次子查询,总共执行 1 + N 次 SQL。

示例

java 复制代码
List<Car> cars = carMapper.selectAll(); // 查询 10 辆车 (1 次 SQL)
for (Car car : cars) {
    // 访问 parts 时,会触发 10 次额外的 SQL (N 次 SQL)
    System.out.println(car.getParts().size()); 
}

即便开启了延迟加载,N+1 次查询依然会发生,只不过从"启动时立即发生"推迟到了"遍历访问时发生"。

4.2 解决方案:使用嵌套结果(JOIN)

这是解决 N+1 问题的唯一根本方法。

通过一次 JOIN 查询,将所有数据(主表 + 关联表)一次性查出来,MyBatis 通过 resultMap 自动去重并组装成对象树。

xml 复制代码
<select id="selectAllWithParts" resultMap="CarPartsMap">
    SELECT c.*, p.*
    FROM t_car c
    LEFT JOIN t_part p ON c.id = p.car_id
</select>

此时只有 1 次 SQL,性能最高。

4.3 策略选择

场景 推荐方案 理由
数据量小 / 必须立即展示 嵌套结果(JOIN) 性能最好,一次查出所有数据
主表数据量大 / 关联字段不常用 嵌套查询 + 延迟加载 节省内存和带宽,按需加载
同时加载多层关联 谨慎使用嵌套查询 容易导致 N+1 爆炸,优先使用 JOIN

五、高级映射与延迟加载最佳实践

5.1 分步查询与延迟加载的实战组合

利用延迟加载,我们可以将复杂的多表关联查询拆分为多个独立的单表查询,提高代码复用性。

Mapper 接口

java 复制代码
public interface CarMapper {
    Car selectById(Long id); // 只查 Car
}
public interface EngineMapper {
    Engine selectByCarId(Long carId); // 只查 Engine
}

XML 组合

xml 复制代码
<resultMap id="CarMap" type="car">
    <id column="id" property="id"/>
    <!-- 此处相当于在 CarMapper 中调用了 EngineMapper 的方法 -->
    <association property="engine" 
                 select="com.xie.mapper.EngineMapper.selectByCarId" 
                 column="id" 
                 fetchType="lazy"/>
</resultMap>

这种写法使得 SQL 逻辑清晰,且 selectByCarId 方法可以在其他地方复用。

5.2 警惕 aggressiveLazyLoading

在旧版 MyBatis 中,如果 aggressiveLazyLoading=true,只要调用了主对象的任何 getter 方法(即使不是关联属性的 getter),都会触发所有延迟加载关联的查询。

建议 :在 MyBatis 3.x 及以上,保持 aggressiveLazyLoading=false(虽然默认已改为 false,但最好显式配置),仅在访问具体关联对象时触发查询。

5.3 在 Spring 环境中处理 Session 问题

延迟加载需要 SqlSession 保持开启状态。如果在 Spring MVC 的 Controller 中直接返回实体类,而视图层渲染时才触发延迟加载,此时 Session 可能已关闭,会抛出 LazyInitializationException

解决方案

  1. 使用 Spring 的 OpenSessionInViewFilter(OSIV)模式,将 Session 生命周期延长到请求结束。
  2. 在 Service 层使用 @Transactional,确保事务提交前完成所有关联对象的加载(触发加载)。

六、常见问题排查

异常信息 原因 解决方案
LazyInitializationException Session 已关闭后尝试加载懒加载数据 配置 OSIV,或确保在 Session 开启期间访问关联数据
明明配置了 lazy=true,但还是立即执行了查询 aggressiveLazyLoading=true 或调用了 hashCode/equals 等方法 设置 aggressiveLazyLoading=false
一对多关联中数据重复 JOIN 查询时未配置 <id> 标签 务必在 resultMap 中配置 <id>,MyBatis 依靠它去重
查询性能极差 使用嵌套查询且命中 N+1 场景 改用嵌套结果(JOIN)一次性查询

七、总结

知识点 核心要点
一对一(association 使用 javaType,处理主表与附属表的关系
一对多(collection 使用 ofType,处理主表与子表集合的关系
嵌套结果(Nested Results) 使用 JOIN,一次 SQL,性能最优,推荐优先使用
嵌套查询(Nested Queries) 拆分 SQL,代码解耦,但存在 N+1 风险
延迟加载 通过 lazyLoadingEnabled 开启,配合 fetchType 精细化控制
N+1 问题 嵌套查询下的性能陷阱,必须用 JOIN 重构

一句话总结

在 MyBatis 中,优先使用 resultMap + JOIN 进行关联映射(嵌套结果)以获取最佳性能;当关联数据庞大且访问频率低时,借助 select 属性实现分步查询,并开启延迟加载(嵌套查询)来优化内存与响应时间。

掌握高级映射与延迟加载的原理,不仅能让你写出高性能的数据库访问代码,更能让你在系统性能调优时游刃有余。

相关推荐
thefool1122661 小时前
从中序与后序遍历序列构造二叉树
java
吴声子夜歌1 小时前
Java面试题——JVM(二)
java·开发语言·jvm
CRMEB1 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php
denglinqings1 小时前
Java开发从入门到精通所有课程
java·开发语言
mashirro1 小时前
记一次基于ruoyi-vue新增小程序服务模块,小程序登陆相关
vue.js·小程序·mybatis
星空1 小时前
maven镜像
java·maven
chuan.bai2 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法
SL_staff2 小时前
销售知识管理的断点分析与技术解法:从3天找话术到秒级检索
java·开源·github
荣码2 小时前
向量数据库选型:4种方案全测了,选错重来成本最高
java·python