resultMap详解
导学
本节学习目标:
- 理解
resultMap与resultType的本质区别,明确何时必须手写resultMap - 掌握
id与result子元素的分工,以及它们对缓存和嵌套映射的影响 - 学会使用
constructor/idArg/arg进行构造方法注入 - 理解
autoMapping三种行为模式(NONE/PARTIAL/FULL)的适用场景 - 掌握
extends继承机制,实现resultMap的复用与分层设计 - 学会使用
columnPrefix处理列名前缀冲突
定义
resultMap 是 MyBatis 中最强大的结果映射元素。当数据库列名与 Java 属性名不一致、需要映射复杂类型、或者需要开启级联关联查询时,resultType 的自动驼峰转换已无法满足需求,此时必须显式定义 resultMap。
它解决的核心痛点:
- 列名与属性名不一致(如
user_name→userName) - 需要精确控制对象标识符,提升一级缓存和嵌套映射的去重性能
- 需要将查询结果映射到不可变对象(通过构造方法注入)
- 需要复用映射规则,避免重复 XML 配置
适用位置与核心属性
resultMap 定义在映射文件的 <mapper> 根元素下,其语法骨架如下:
<resultMap id="唯一标识" type="Java全限定类名或别名" extends="父resultMapId"
autoMapping="PARTIAL">
<constructor>
<idArg column="" javaType=""/>
<arg column="" javaType=""/>
</constructor>
<id property="" column=""/>
<result property="" column=""/>
</resultMap>
| 属性 / 子元素 | 必填 | 说明 |
|---|---|---|
id | 是 | resultMap 在当前命名空间内的唯一标识,供 select 等语句引用 |
type | 是 | 映射的目标 Java 类型(类全名或别名) |
extends | 否 | 继承另一个 resultMap 的全部映射规则,支持多层继承 |
autoMapping | 否 | 自动映射行为:NONE(关闭)、PARTIAL(默认,仅简单属性)、FULL(包含嵌套) |
<id> | 否 | 标记对象标识符列,MyBatis 用它判断结果集中的两行是否代表同一个对象实例 |
<result> | 否 | 普通属性与列的映射 |
<constructor> | 否 | 通过构造方法创建对象,内含 <idArg> 和 <arg> |
<idArg> | 否 | 构造方法中的标识符参数 |
<arg> | 否 | 构造方法中的普通参数 |
columnPrefix | 否 | 在 <association> / <collection> 中使用,为关联列自动添加前缀 |
id 与 result 的本质区别
<id>:对应数据库主键或业务唯一键。MyBatis 在解析结果集时,利用<id>列的值判断两行数据是否属于同一个 Java 对象。如果两行id相同,MyBatis 会认为它们是同一个对象,后续只会填充其关联属性,而不会重复创建新实例。这对一级缓存、嵌套association和collection的去重至关重要。<result>:对应普通属性。仅完成列值到属性值的映射,不参与对象身份判定。
autoMapping 三模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
NONE | 关闭自动映射,所有属性必须显式配置 | 需要绝对控制映射过程,避免意外字段注入 |
PARTIAL | 自动映射简单属性(非嵌套),但遇到已显式映射的属性会跳过 | 默认推荐,平衡便利与可控性 |
FULL | 自动映射所有属性,包括嵌套对象的简单属性 | 快速原型开发,但可能引发列名冲突 |
核心原理
resultMap 解析与映射流程
MyBatis 执行查询后,结果集的处理并非简单的“一行转一个对象”。当存在 resultMap 时,框架会经历以下阶段:
流程说明:
- 查找
resultMap:select语句通过resultMap属性定位定义。 - 对象身份判定:若配置了
<id>,MyBatis 内部维护一个Map<idValue, Object>,相同id的行会被合并到同一对象。 - 属性填充:
<result>和自动映射的列按类型处理器(TypeHandler)完成转换后写入属性。 - 构造方法分支:若配置了
<constructor>,MyBatis 直接调用匹配的构造方法,跳过无参构造 + setter 模式。
完整示例
场景说明
乐途公司员工信息表中,列名采用下划线命名(emp_name、dept_code),而 Java 实体采用驼峰命名(empName、deptCode)。此外,员工实体包含一个不可变的 id 字段,希望通过构造方法注入。我们将演示:
- 基础
resultMap(id+result) extends复用autoMapping控制columnPrefix处理关联列前缀
操作前的数据库表结构及初始数据
-- 员工表
CREATE TABLE employee (
emp_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工编号',
emp_name VARCHAR(20) COMMENT '姓名',
emp_age INT COMMENT '年龄',
dept_code VARCHAR(10) COMMENT '部门编码',
hire_date DATE COMMENT '入职日期'
);
-- 部门表
CREATE TABLE department (
dept_code VARCHAR(10) PRIMARY KEY COMMENT '部门编码',
dept_name VARCHAR(20) COMMENT '部门名称'
);
初始数据:
| emp_id | emp_name | emp_age | dept_code | hire_date |
|---|---|---|---|---|
| 1 | 大翔 | 28 | D01 | 2020-03-15 |
| 2 | 白歌 | 25 | D01 | 2021-06-01 |
| 3 | 小崔 | 30 | D02 | 2019-11-20 |
| dept_code | dept_name |
|---|---|
| D01 | 研发部 |
| D02 | 产品部 |
Java 实体类
package com.flywing.entity;
import java.time.LocalDate;
public class Employee {
private final Integer id; // 通过构造方法注入
private String empName;
private Integer empAge;
private String deptCode;
private LocalDate hireDate;
private Department department; // 关联对象
public Employee(Integer id) {
this.id = id;
}
// getter / setter 省略...
public Integer getId() { return id; }
}
public class Department {
private String deptCode;
private String deptName;
// getter / setter 省略...
}
完整的映射文件片段
<?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.flywing.mapper.EmployeeMapper">
<!-- 基础resultMap:演示id、result、constructor -->
<resultMap id="BaseEmployeeMap" type="com.flywing.entity.Employee"
autoMapping="NONE">
<!-- 通过构造方法注入不可变id -->
<constructor>
<idArg column="emp_id" javaType="java.lang.Integer"/>
</constructor>
<!-- 对象标识符:告诉MyBatis emp_id是主键,用于去重 -->
<id property="id" column="emp_id"/>
<result property="empName" column="emp_name"/>
<result property="empAge" column="emp_age"/>
<result property="deptCode" column="dept_code"/>
<result property="hireDate" column="hire_date"
javaType="java.time.LocalDate"
typeHandler="org.apache.ibatis.type.LocalDateTypeHandler"/>
</resultMap>
<!-- 继承基础Map,添加部门关联,演示extends与columnPrefix -->
<resultMap id="EmployeeWithDeptMap" type="com.flywing.entity.Employee"
extends="BaseEmployeeMap"
autoMapping="NONE">
<association property="department" javaType="com.flywing.entity.Department"
columnPrefix="d_">
<id property="deptCode" column="dept_code"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
<!-- 查询1:基础映射 -->
<select id="selectById" resultMap="BaseEmployeeMap">
SELECT emp_id, emp_name, emp_age, dept_code, hire_date
FROM employee
WHERE emp_id = #{id}
</select>
<!-- 查询2:带部门信息,使用JOIN,columnPrefix自动匹配d_前缀 -->
<select id="selectWithDept" resultMap="EmployeeWithDeptMap">
SELECT
e.emp_id,
e.emp_name,
e.emp_age,
e.dept_code,
e.hire_date,
d.dept_code AS d_dept_code,
d.dept_name AS d_dept_name
FROM employee e
LEFT JOIN department d ON e.dept_code = d.dept_code
WHERE e.emp_id = #{id}
</select>
</mapper>
实际执行结果
查询1 结果(selectById 返回 BaseEmployeeMap):
| emp_id | emp_name | emp_age | dept_code | hire_date |
|---|---|---|---|---|
| 1 | 大翔 | 28 | D01 | 2020-03-15 |
控制台 SQL 输出:
==> Preparing: SELECT emp_id, emp_name, emp_age, dept_code, hire_date FROM employee WHERE emp_id = ?
==> Parameters: 1(Integer)
<== Columns: emp_id, emp_name, emp_age, dept_code, hire_date
<== Row: 1, 大翔, 28, D01, 2020-03-15
<== Total: 1
查询2 结果(selectWithDept 返回 EmployeeWithDeptMap):
| emp_id | emp_name | emp_age | dept_code | hire_date | department.dept_code | department.dept_name |
|---|---|---|---|---|---|---|
| 1 | 大翔 | 28 | D01 | 2020-03-15 | D01 | 研发部 |
控制台 SQL 输出:
==> Preparing: SELECT e.emp_id, e.emp_name, e.emp_age, e.dept_code, e.hire_date, d.dept_code AS d_dept_code, d.dept_name AS d_dept_name FROM employee e LEFT JOIN department d ON e.dept_code = d.dept_code WHERE e.emp_id = ?
==> Parameters: 1(Integer)
<== Columns: emp_id, emp_name, emp_age, dept_code, hire_date, d_dept_code, d_dept_name
<== Row: 1, 大翔, 28, D01, 2020-03-15, D01, 研发部
<== Total: 1
分析
<id>的作用:在BaseEmployeeMap中,emp_id被标记为<id>。如果后续查询返回多行且emp_id相同(例如一对多 JOIN),MyBatis 不会重复创建Employee对象,而是复用已有实例,仅填充其集合属性。这直接决定了关联查询的正确性。extends的价值:EmployeeWithDeptMap继承了BaseEmployeeMap的全部映射规则,只需额外声明association。当实体属性很多时,这种分层设计能显著减少重复 XML。columnPrefix的便利:在 JOIN 查询中,部门表的列被别名化为d_dept_code、d_dept_name。columnPrefix="d_"让association内部只需写原始列名dept_code,MyBatis 会自动拼接前缀查找,避免列名冲突。autoMapping="NONE":本示例关闭了自动映射,所有属性必须显式声明。这在企业级开发中更安全,能防止数据库新增敏感列被意外映射到实体。
易错场景 / 常见误区
| 误区 | 正解 |
|---|---|
认为 <id> 只是语法糖,用 <result> 代替也能正常工作 | <id> 参与对象身份判定,缺失会导致关联查询时重复创建对象,破坏去重逻辑 |
extends 继承后,子 resultMap 中重复定义父级已映射的属性 | 子 resultMap 只需定义新增属性;重复定义会覆盖父级规则,可能引发意外行为 |
autoMapping="FULL" 下,嵌套对象的属性也被自动映射,导致列名冲突 | 多表 JOIN 时,列名容易重复,建议显式配置或使用 columnPrefix,慎用 FULL |
构造方法注入时,<idArg> 和 <arg> 的顺序与 Java 构造参数顺序不一致 | MyBatis 按 XML 声明顺序匹配构造参数,顺序错误会导致类型不匹配或注入错误 |
以为 resultMap 的 id 属性是全局唯一的 | resultMap 的 id 仅在当前 Mapper 命名空间内唯一,跨命名空间引用需加前缀 |
面试考点
Q1:<id> 和 <result> 在 MyBatis 内部处理上有什么区别?为什么关联查询时必须配置 <id>?
A:
<id>标记的列被 MyBatis 用作对象标识符。内部有一个Map缓存,键为<id>列的值,值为已创建的 Java 对象。当结果集中出现相同<id>的多行(如一对多 JOIN),MyBatis 会复用该对象实例,仅向其集合属性追加元素。如果用<result>代替<id>,每行都会创建新对象,导致主对象重复、集合属性丢失,查询结果条数异常膨胀。
Q2:autoMapping 的 PARTIAL 和 FULL 有什么区别?生产环境推荐哪种?
A:
PARTIAL是默认值,只对简单属性(非嵌套对象)进行自动映射,且不会覆盖已显式配置的属性;FULL会对嵌套对象的简单属性也进行自动映射。生产环境推荐PARTIAL或显式关闭(NONE),因为FULL在多表 JOIN 场景下极易因列名重复导致数据被错误注入。
Q3:extends 继承 resultMap 时,如果父级和子级都定义了同一个 <result>,以谁为准?
A:子级定义会覆盖父级定义。MyBatis 在合并继承链时,后加载的子级属性会替换先加载的父级同名属性。因此继承时应避免重复定义同一属性,除非确实需要覆盖。
Q4:什么情况下必须使用 resultMap,而不能用 resultType?
A:以下四种场景必须手写
resultMap:(1)列名与属性名不一致且未开启全局驼峰映射;(2)需要映射复杂类型(如枚举、自定义 TypeHandler);(3)需要配置association/collection/discriminator等嵌套映射;(4)需要通过构造方法创建不可变对象。
小结
resultMap 是 MyBatis 结果映射体系的基石。<id> 与 <result> 的分工体现了框架对“对象身份”的精确管理;extends 提供了映射规则的分层复用能力;autoMapping 则在便利性与可控性之间提供了三档选择。掌握这些基础后,才能正确理解后续 association、collection 等高级映射的行为边界。
下一章引子
基础映射规则已就绪,但真实业务中对象很少孤立存在。下一节将深入讲解 一对一关联查询 association,对比嵌套 Select 与嵌套结果映射两种实现方式,并揭示它们对性能的潜在影响。