MyBatis resultMap 结果映射完全指南

MyBatis resultMap 结果映射完全指南

一、resultMap 是什么?

<resultMap> 是 MyBatis 中最核心、最强大的映射标签,它定义了 数据库查询结果集(ResultSet)如何映射到 Java 对象。它可以处理从最简单的列名与属性名不一致,到最复杂的一对多、多对多关联映射等各种场景。

1.1 为什么需要 resultMap?

需求场景 resultType 能否解决 resultMap 能否解决
列名与属性名完全一致 ✅ 可以 ✅ 可以
列名与属性名不一致(如下划线转驼峰) ⚠️ 需开启设置或用别名 ✅ 原生支持
一对一关联查询(Car → Engine) ❌ 不支持 ✅ 支持
一对多关联查询(Car → List<Part>) ❌ 不支持 ✅ 支持
自定义类型转换(如 JSON 字段) ❌ 不支持 ✅ 支持
继承关系映射 ❌ 不支持 ✅ 支持
使用二级缓存 ❌ 不支持 ✅ 支持

一句话总结resultType 是快速通道,resultMap 是万能通道。


二、resultMap 基本结构

2.1 语法模板

xml 复制代码
<resultMap id="唯一标识" type="映射的目标类" autoMapping="true/false" extends="父resultMap的id">
    <!-- 1. 构造方法映射(可选) -->
    <constructor>
        <idArg column="数据库列名" javaType="Java类型"/>
        <arg column="数据库列名" javaType="Java类型"/>
    </constructor>
    
    <!-- 2. 主键映射(推荐配置,优化性能) -->
    <id property="Java属性名" column="数据库列名"/>
    
    <!-- 3. 普通字段映射 -->
    <result property="Java属性名" column="数据库列名"/>
    
    <!-- 4. 一对一关联映射 -->
    <association property="关联属性名" javaType="关联类型">
        <!-- 子映射 -->
    </association>
    
    <!-- 5. 一对多关联映射 -->
    <collection property="集合属性名" ofType="集合元素类型">
        <!-- 子映射 -->
    </collection>
    
    <!-- 6. 鉴别器(多态映射) -->
    <discriminator javaType="string" column="鉴别列">
        <case value="值1" resultMap="映射1"/>
        <case value="值2" resultMap="映射2"/>
    </discriminator>
</resultMap>

2.2 核心子元素详解

子元素 作用 使用场景
<constructor> 通过构造方法创建对象 实体类没有无参构造方法时
<id> 映射主键列 所有场景(推荐配置,提升性能)
<result> 映射普通列 所有场景
<association> 一对一关联 对象中包含另一个对象(如 Car → Engine)
<collection> 一对多关联 对象中包含集合(如 Car → List<Part>)
<discriminator> 根据列值选择不同映射策略 多态映射场景

2.3 常用属性说明

属性 作用 示例
id resultMap 的唯一标识 id="carMap"
type 映射的目标 Java 类型 type="com.xie.entity.Car"type="car"(需配置别名)
autoMapping 是否自动映射未显式配置的列 autoMapping="true"(默认)
extends 继承另一个 resultMap extends="baseMap"

三、基本映射:处理列名不一致

3.1 场景:数据库列名与 Java 属性名不同

数据库表使用下划线命名(car_num),Java 使用驼峰命名(carNum)。

实体类

java 复制代码
public class Car {
    private Long id;
    private String carNum;      // 对应 car_num
    private String brand;
    private Double guidePrice;  // 对应 guide_price
    private String produceTime; // 对应 produce_time
    private String carType;     // 对应 car_type
    // getter/setter
}

方式一:使用 resultMap(推荐,一劳永逸)

xml 复制代码
<resultMap id="carMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="guidePrice" column="guide_price"/>
    <result property="produceTime" column="produce_time"/>
    <result property="carType" column="car_type"/>
    <!-- brand 列名与属性名一致,可以不配置,自动映射 -->
</resultMap>

<select id="selectById" resultMap="carMap">
    SELECT * FROM t_car WHERE id = #{id}
</select>

方式二:开启驼峰转换(全局配置,更简单)

xml 复制代码
<!-- mybatis-config.xml -->
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

开启后,car_num 自动映射到 carNumguide_price 映射到 guidePrice

方式三:SQL 中使用别名(临时方案)

xml 复制代码
<select id="selectById" resultType="car">
    SELECT 
        id, 
        car_num AS carNum, 
        guide_price AS guidePrice
    FROM t_car WHERE id = #{id}
</select>

3.2 autoMapping 自动映射

xml 复制代码
<!-- autoMapping="true"(默认):未配置的列自动映射 -->
<resultMap id="carMap" type="car" autoMapping="true">
    <!-- 只配置特殊映射,其他列自动匹配 -->
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <!-- brand、guidePrice 等自动映射 -->
</resultMap>

<!-- autoMapping="false":所有列必须显式配置 -->
<resultMap id="carMap" type="car" autoMapping="false">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>        <!-- 必须配置 -->
    <result property="guidePrice" column="guide_price"/> <!-- 必须配置 -->
</resultMap>

建议 :保持 autoMapping="true"(默认),只配置特殊映射,减少冗余代码。


四、一对一关联映射(association)

4.1 场景描述

每辆车有一个引擎(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;      // 发动机类型:V6、V8、电动
    private Integer power;    // 功率(kW)
    private Double displacement; // 排量(L)
    // getter/setter
}

4.2 方式一:嵌套查询(Nested Query)

先查 Car,再通过子查询查 Engine。会产生 N+1 查询 问题,需谨慎使用。

xml 复制代码
<resultMap id="carWithEngineMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- 一对一:通过另一个查询获取引擎信息 -->
    <association property="engine" 
                 javaType="Engine"
                 select="com.xie.mapper.EngineMapper.selectByCarId"
                 column="id"
                 fetchType="lazy"/>  <!-- lazy:懒加载;eager:立即加载 -->
</resultMap>

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

对应的 EngineMapper

xml 复制代码
<select id="selectByCarId" resultType="Engine">
    SELECT * FROM t_engine WHERE car_id = #{carId}
</select>

优缺点

  • ✅ 优点:主查询 SQL 简单,易于维护。
  • ❌ 缺点:N+1 查询,当查询 10 辆车时会执行 11 次 SQL。

4.3 方式二:嵌套结果(Nested Result)⭐ 推荐

一次 JOIN 查询,把所有数据查出来,性能最优。

xml 复制代码
<resultMap id="carWithEngineMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- 一对一:从 JOIN 结果中直接映射 -->
    <association property="engine" javaType="Engine">
        <id property="id" column="engine_id"/>
        <result property="type" column="engine_type"/>
        <result property="power" column="engine_power"/>
        <result property="displacement" column="engine_displacement"/>
    </association>
</resultMap>

<select id="selectCarWithEngine" resultMap="carWithEngineMap">
    SELECT 
        c.id, 
        c.car_num, 
        c.brand,
        e.id AS engine_id, 
        e.type AS engine_type, 
        e.power AS engine_power,
        e.displacement AS engine_displacement
    FROM t_car c
    LEFT JOIN t_engine e ON c.id = e.car_id
    WHERE c.id = #{id}
</select>

关键点 :多表 JOIN 时,所有列必须使用别名,避免同名列覆盖。


五、一对多关联映射(collection)

5.1 场景描述

一辆车有多个配件(Part),查询车辆时需要同时加载配件列表。

实体类

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

public class Part {
    private Long id;
    private String name;      // 配件名称
    private Double price;     // 配件价格
    private String supplier;  // 供应商
    // getter/setter
}

5.2 方式一:嵌套查询

xml 复制代码
<resultMap id="carWithPartsMap" 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"/>
</resultMap>

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

注意collection 使用 ofType 指定集合中元素的类型,而不是 javaType

5.3 方式二:嵌套结果 ⭐ 推荐

xml 复制代码
<resultMap id="carWithPartsMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- 一对多:从 JOIN 结果中直接映射 -->
    <collection property="parts" ofType="Part">
        <id property="id" column="part_id"/>
        <result property="name" column="part_name"/>
        <result property="price" column="part_price"/>
        <result property="supplier" column="part_supplier"/>
    </collection>
</resultMap>

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

⚠️ 关键点<id> 标签在 collection 中用于标识主键,MyBatis 通过它来判断记录是否属于同一个 Car 对象,必须配置,否则会导致数据重复。

5.4 一对多查询中的去重原理

当查询结果有多行时,MyBatis 通过 <id> 标签判断哪些行属于同一个父对象。

ini 复制代码
查询结果(3行):
car_id=1, car_num=A001, part_id=101, part_name=轮胎
car_id=1, car_num=A001, part_id=102, part_name=方向盘
car_id=2, car_num=A002, part_id=201, part_name=电池

MyBatis 处理逻辑:
1. 读取第1行:car_id=1 → 创建 Car 对象,添加到 parts 列表
2. 读取第2行:car_id=1 → 发现已存在,只创建 Part 对象并加入 parts 列表
3. 读取第3行:car_id=2 → 创建新的 Car 对象

如果没有 <id>,MyBatis 无法识别同一行,每行都会创建新的 Car 对象,导致数据重复。


六、多层嵌套映射

实际业务中,经常需要多层关联映射:Car → Engine → Part(引擎也有配件)。

xml 复制代码
<resultMap id="carFullMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    
    <!-- 第一层:Car 关联 Engine -->
    <association property="engine" javaType="Engine">
        <id property="id" column="engine_id"/>
        <result property="type" column="engine_type"/>
        <result property="power" column="engine_power"/>
        
        <!-- 第二层:Engine 关联 Part -->
        <collection property="parts" ofType="Part">
            <id property="id" column="engine_part_id"/>
            <result property="name" column="engine_part_name"/>
        </collection>
    </association>
    
    <!-- Car 关联自身的 Parts -->
    <collection property="parts" ofType="Part">
        <id property="id" column="car_part_id"/>
        <result property="name" column="car_part_name"/>
    </collection>
</resultMap>

七、鉴别器(discriminator)

根据某个列的值,选择不同的映射策略(多态映射)。

7.1 场景描述

车辆有燃油车和电动车两种类型,它们有不同字段。

实体类

java 复制代码
public class Car {
    private Long id;
    private String carNum;
    private String brand;
    private String carType;  // 燃油车 / 电动车
    // 燃油车特有
    private Double fuelConsumption;  // 油耗
    // 电动车特有
    private Double batteryCapacity;  // 电池容量
}

7.2 鉴别器配置

xml 复制代码
<resultMap id="carDiscriminatorMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
    <result property="carType" column="car_type"/>
    
    <discriminator javaType="string" column="car_type">
        <case value="燃油车" resultMap="fuelCarMap"/>
        <case value="电动车" resultMap="electricCarMap"/>
    </discriminator>
</resultMap>

<resultMap id="fuelCarMap" type="car" extends="carDiscriminatorMap">
    <result property="fuelConsumption" column="fuel_consumption"/>
</resultMap>

<resultMap id="electricCarMap" type="car" extends="carDiscriminatorMap">
    <result property="batteryCapacity" column="battery_capacity"/>
</resultMap>

查询 SQL

sql 复制代码
SELECT id, car_num, brand, car_type, 
       fuel_consumption, battery_capacity 
FROM t_car

car_type = '燃油车' 时,使用 fuelCarMap,映射 fuelConsumption。 当 car_type = '电动车' 时,使用 electricCarMap,映射 batteryCapacity


八、继承与复用(extends)

通过 extends 属性继承另一个 resultMap,避免重复配置。

xml 复制代码
<!-- 基础映射 -->
<resultMap id="baseCarMap" type="car">
    <id property="id" column="id"/>
    <result property="carNum" column="car_num"/>
    <result property="brand" column="brand"/>
</resultMap>

<!-- 扩展映射1:带引擎 -->
<resultMap id="carWithEngineMap" type="car" extends="baseCarMap">
    <association property="engine" javaType="Engine">
        <id property="id" column="engine_id"/>
        <result property="type" column="engine_type"/>
    </association>
</resultMap>

<!-- 扩展映射2:带配件 -->
<resultMap id="carWithPartsMap" type="car" extends="baseCarMap">
    <collection property="parts" ofType="Part">
        <id property="id" column="part_id"/>
        <result property="name" column="part_name"/>
    </collection>
</resultMap>

九、构造方法映射(constructor)

当实体类没有无参构造方法或想通过构造方法注入属性时使用。

xml 复制代码
<resultMap id="carConstructorMap" type="car">
    <constructor>
        <idArg column="id" javaType="Long"/>
        <arg column="car_num" javaType="String"/>
        <arg column="brand" javaType="String"/>
    </constructor>
</resultMap>

对应的实体类:

java 复制代码
public class Car {
    private Long id;
    private String carNum;
    private String brand;
    
    public Car(Long id, String carNum, String brand) {
        this.id = id;
        this.carNum = carNum;
        this.brand = brand;
    }
}

十、性能优化:避免 N+1 查询

10.1 N+1 问题是什么?

使用嵌套查询时:

  1. 执行 1 次主查询,得到 N 条记录。
  2. 对每条记录执行 1 次子查询,共 N 次。
  3. 总计 N+1 次查询。

10.2 解决方案

方案一:使用嵌套结果(JOIN)⭐ 推荐

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

方案二:使用懒加载(部分场景优化)

xml 复制代码
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
xml 复制代码
<association property="engine" 
             javaType="Engine"
             select="com.xie.mapper.EngineMapper.selectByCarId"
             column="id"
             fetchType="lazy"/>  <!-- 懒加载,需要时才查 -->

方案三:使用全局配置的懒加载

xml 复制代码
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

十一、常见易错点与避坑指南

11.1 collection 的 ofType vs javaType

xml 复制代码
<!-- ❌ 错误:collection 不能用 javaType -->
<collection property="parts" javaType="List" ofType="Part">

<!-- ✅ 正确:collection 用 ofType 指定元素类型 -->
<collection property="parts" ofType="Part">

11.2 忘记配置 <id> 导致数据重复

xml 复制代码
<!-- ❌ 错误:缺少 id,导致同一个 Car 对象出现多次 -->
<collection property="parts" ofType="Part">
    <result property="name" column="part_name"/>
</collection>

<!-- ✅ 正确:配置 id 标识唯一性 -->
<collection property="parts" ofType="Part">
    <id property="id" column="part_id"/>
    <result property="name" column="part_name"/>
</collection>

11.3 多表 JOIN 列名冲突

sql 复制代码
-- 错误:两表都有 id 列,会覆盖
SELECT c.*, e.* FROM t_car c JOIN t_engine e ON c.id = e.car_id

-- 正确:使用别名区分
SELECT 
    c.id AS car_id, 
    e.id AS engine_id,
    ...
FROM t_car c JOIN t_engine e ON c.id = e.car_id

11.4 嵌套查询的懒加载失效

SqlSession 关闭后,再访问懒加载的属性会报错。解决:在事务中完成所有操作,或使用 Spring 的 @Transactional


十二、resultMap 与 resultType 选择指南

场景 推荐方案 原因
列名与属性名一致 resultType 最简洁
列名与属性名不一致(差异少) resultType + SQL 别名 简单高效
列名与属性名不一致(差异多) resultMap 一劳永逸
一对一关联查询(JOIN 一次完成) resultMap + association(嵌套结果) 性能最优
一对多关联查询(JOIN 一次完成) resultMap + collection(嵌套结果) 性能最优
需要懒加载关联数据 resultMap + association/collection(嵌套查询 + fetchType="lazy" 按需加载
需要自定义类型转换 resultMap + TypeHandler 灵活控制
需要使用二级缓存 resultMap 缓存需要 resultMap
快速原型开发 resultType 快速简单
生产项目(复杂映射) resultMap 规范、可维护

十三、最佳实践总结

  1. 关联查询优先使用嵌套结果(JOIN):一次查询,性能最优。
  2. collection 中务必配置 <id>:避免数据重复。
  3. 多表 JOIN 时所有列必须使用别名:防止同名列覆盖。
  4. 合理使用 extends 复用映射配置:减少重复代码。
  5. 开启 autoMapping="true"(默认):只配置特殊映射,简化配置。
  6. 对于复杂聚合查询,适当使用 discriminator:实现多态映射。
  7. 注意 N+1 查询问题:合理使用 JOIN 或懒加载。
  8. 单元测试验证映射正确性:确保每个字段都正确映射。

resultMap 是 MyBatis 最强大的特性之一,熟练掌握它,可以轻松应对 90% 以上的复杂映射场景,写出高性能、易维护的数据访问层代码。希望本文能帮助你彻底掌握这一核心知识点!

相关推荐
nnerddboy1 小时前
Rust教程03:函数,控制流与所有权
开发语言·后端·rust
山荷枝1 小时前
04-框架--SpringBoot
java·spring boot·后端
Undoom2 小时前
用TextIn xParse搞定批量简历解析,WorkBuddy里一句话就能完成
后端
乐橙开放平台3 小时前
一周上线校园透明化:listDeviceDetailsByPage 台账 + getKitToken + ImouPlayer 多路墙
后端·物联网·安全·音视频·智能家居
樊小肆4 小时前
# 你还在等DeepSeek官方 agent Harness‌? 来试试 DeepSeeker-Code吧
前端·人工智能·后端
樊小肆4 小时前
2568 万 token 才花 2 块 2:聊聊 DeepSeeker-Code 怎么吃满上下文缓存
前端·人工智能·后端
众人皆醒我独醉4 小时前
大模型训练优化:FSDP、DeepSpeed ZeRO 与混合精度
后端·面试·gpu
Zane19944 小时前
ClassName() 只是一步?拆开看 __new__ 和 __init__ 各自在干什么
后端·python
geovindu4 小时前
java: Memento Pattern
java·开发语言·后端·备忘录模式·行为模式