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 自动映射到 carNum,guide_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 次主查询,得到 N 条记录。
- 对每条记录执行 1 次子查询,共 N 次。
- 总计 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 |
规范、可维护 |
十三、最佳实践总结
- 关联查询优先使用嵌套结果(JOIN):一次查询,性能最优。
- 在
collection中务必配置<id>:避免数据重复。 - 多表 JOIN 时所有列必须使用别名:防止同名列覆盖。
- 合理使用
extends复用映射配置:减少重复代码。 - 开启
autoMapping="true"(默认):只配置特殊映射,简化配置。 - 对于复杂聚合查询,适当使用
discriminator:实现多态映射。 - 注意 N+1 查询问题:合理使用 JOIN 或懒加载。
- 单元测试验证映射正确性:确保每个字段都正确映射。
resultMap 是 MyBatis 最强大的特性之一,熟练掌握它,可以轻松应对 90% 以上的复杂映射场景,写出高性能、易维护的数据访问层代码。希望本文能帮助你彻底掌握这一核心知识点!