resultMap
导学
本节将深入掌握MyBatis中最强大的结果映射机制。你将学会resultMap的完整结构,理解id与result子元素的区别,掌握autoMapping的行为控制,了解extends继承机制,并能够用resultMap解决列名与属性名不一致的映射难题。association、collection、discriminator等高级特性将放到第05章讲解。
定义
resultMap是SQL映射文件中用于显式定义结果集与Java对象映射关系的元素。在JDBC原始写法中,当数据库列名与Java属性名不一致时,开发者需要在ResultSet遍历代码中硬编码列名与属性的对应关系,代码冗长且难以维护。resultMap将这种对应关系声明化:在XML中一次性定义好"哪一列映射到哪个属性",所有引用该resultMap的查询都会遵循这一规则,实现映射逻辑的集中管理与复用。
适用位置与核心属性
resultMap元素书写在映射文件的<mapper>根标签内部,可被多个select元素通过resultMap属性引用。
| 属性 | 是否必填 | 说明 |
|---|---|---|
id | 是 | resultMap的唯一标识,供select元素的resultMap属性引用 |
type | 是 | 映射目标Java类的全限定名或别名 |
autoMapping | 否 | 是否开启自动映射,默认根据全局配置(通常为true) |
extends | 否 | 继承另一个resultMap的映射规则,实现复用 |
resultMap内部子元素(本章重点讲解前两个):
| 子元素 | 说明 |
|---|---|
<id> | 标识主键列与属性的映射,用于缓存和结果集唯一性判断 |
<result> | 标识普通列与属性的映射 |
<constructor> | 通过构造函数参数映射(第05章讲解) |
<association> | 一对一关联映射(第05章讲解) |
<collection> | 一对多关联映射(第05章讲解) |
<discriminator> | 鉴别器映射(第05章讲解) |
核心原理
MyBatis解析resultMap时,会构建一棵映射规则树,在结果集处理阶段按规则逐行、逐列地将JDBC数据转换为Java对象。
- 解析阶段:MyBatis启动时解析XML,将每个
<id>和<result>转换为ResultMapping对象,注册到Configuration中。 - 引用阶段:
select元素通过resultMap属性找到对应的映射规则。 - 结果集处理:
ResultSetHandler遍历ResultSet的每一行。 - 对象创建或复用:根据
<id>元素指定的主键列判断当前行是否属于已创建的对象(对普通查询而言,每行都是新对象)。 - 显式映射:先处理
<id>,再处理<result>,通过反射调用setter方法赋值。 - 自动映射补充:若开启
autoMapping,未在resultMap中显式声明但列名与属性名匹配的列会被自动映射。 - 返回:所有行处理完毕后,返回对象列表。
完整示例
场景说明
乐途公司技术部的数据库表采用下划线命名(stu_id、stu_name、stu_age),但Java实体类采用驼峰命名(stuId、stuName、stuAge)。resultType无法处理这种差异,必须通过resultMap显式建立映射关系。此外,系统还需要复用基础映射规则扩展出包含成绩信息的完整映射。
操作前的数据库表结构及初始数据
CREATE TABLE student (
stu_id INT PRIMARY KEY AUTO_INCREMENT,
stu_name VARCHAR(20),
stu_age INT,
stu_major VARCHAR(20),
stu_score DECIMAL(5,2)
);
INSERT INTO student (stu_name, stu_age, stu_major, stu_score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
当前数据状态:
| stu_id | stu_name | stu_age | stu_major | stu_score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整的映射文件片段与Java代码
POJO类
package com.flywing.entity;
public class Student {
private Integer stuId;
private String stuName;
private Integer stuAge;
private String stuMajor;
private Double stuScore;
// Getter与Setter
public Integer getStuId() { return stuId; }
public void setStuId(Integer stuId) { this.stuId = stuId; }
public String getStuName() { return stuName; }
public void setStuName(String stuName) { this.stuName = stuName; }
public Integer getStuAge() { return stuAge; }
public void setStuAge(Integer stuAge) { this.stuAge = stuAge; }
public String getStuMajor() { return stuMajor; }
public void setStuMajor(String stuMajor) { this.stuMajor = stuMajor; }
public Double getStuScore() { return stuScore; }
public void setStuScore(Double stuScore) { this.stuScore = stuScore; }
}
Mapper接口
package com.flywing.mapper;
import com.flywing.entity.Student;
import java.util.List;
public interface StudentMapper {
// 使用基础resultMap查询
Student findById(Integer id);
// 使用继承的resultMap查询
Student findFullById(Integer id);
// 查询列表
List<Student> findAll();
}
映射文件 StudentMapper.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.flywing.mapper.StudentMapper">
<!--
基础resultMap:映射主键和基本信息
id子元素标识主键,result子元素标识普通列
-->
<resultMap id="BaseResultMap" type="com.flywing.entity.Student">
<id column="stu_id" property="stuId" />
<result column="stu_name" property="stuName" />
<result column="stu_age" property="stuAge" />
</resultMap>
<!--
扩展resultMap:继承BaseResultMap,增加专业和成绩映射
extends属性实现规则复用,避免重复书写id和name的映射
-->
<resultMap id="FullResultMap" type="com.flywing.entity.Student" extends="BaseResultMap">
<result column="stu_major" property="stuMajor" />
<result column="stu_score" property="stuScore" />
</resultMap>
<!-- 使用基础resultMap -->
<select id="findById" resultMap="BaseResultMap">
SELECT stu_id, stu_name, stu_age
FROM student
WHERE stu_id = #{id}
</select>
<!-- 使用扩展resultMap -->
<select id="findFullById" resultMap="FullResultMap">
SELECT stu_id, stu_name, stu_age, stu_major, stu_score
FROM student
WHERE stu_id = #{id}
</select>
<!-- 使用扩展resultMap查询列表 -->
<select id="findAll" resultMap="FullResultMap">
SELECT stu_id, stu_name, stu_age, stu_major, stu_score
FROM student
</select>
</mapper>
测试代码
package com.flywing.test;
import com.flywing.entity.Student;
import com.flywing.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class ResultMapTest {
public static void main(String[] args) throws Exception {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);
// 场景一:基础resultMap
Student s1 = mapper.findById(1);
System.out.println("基础resultMap查询:");
System.out.println(" ID=" + s1.getStuId() + ",姓名=" + s1.getStuName() + ",年龄=" + s1.getStuAge());
// 场景二:扩展resultMap
Student s2 = mapper.findFullById(2);
System.out.println("扩展resultMap查询:");
System.out.println(" ID=" + s2.getStuId() + ",姓名=" + s2.getStuName()
+ ",专业=" + s2.getStuMajor() + ",分数=" + s2.getStuScore());
// 场景三:列表查询
List<Student> list = mapper.findAll();
System.out.println("列表查询结果:");
for (Student s : list) {
System.out.println(" " + s.getStuId() + " | " + s.getStuName()
+ " | " + s.getStuMajor() + " | " + s.getStuScore());
}
session.close();
}
}
实际执行结果
控制台SQL输出
[DEBUG] com.flywing.mapper.StudentMapper.findById - ==> Preparing: SELECT stu_id, stu_name, stu_age FROM student WHERE stu_id = ?
[DEBUG] com.flywing.mapper.StudentMapper.findById - ==> Parameters: 1(Integer)
[DEBUG] com.flywing.mapper.StudentMapper.findById - <== Total: 1
基础resultMap查询:
ID=1,姓名=大翔,年龄=22
[DEBUG] com.flywing.mapper.StudentMapper.findFullById - ==> Preparing: SELECT stu_id, stu_name, stu_age, stu_major, stu_score FROM student WHERE stu_id = ?
[DEBUG] com.flywing.mapper.StudentMapper.findFullById - ==> Parameters: 2(Integer)
[DEBUG] com.flywing.mapper.StudentMapper.findFullById - <== Total: 1
扩展resultMap查询:
ID=2,姓名=白歌,专业=软件工程,分数=88.0
[DEBUG] com.flywing.mapper.StudentMapper.findAll - ==> Preparing: SELECT stu_id, stu_name, stu_age, stu_major, stu_score FROM student
[DEBUG] com.flywing.mapper.StudentMapper.findAll - <== Total: 5
列表查询结果:
1 | 大翔 | 计算机科学 | 95.5
2 | 白歌 | 软件工程 | 88.0
3 | 小崔 | 计算机科学 | 92.0
4 | 黄俪 | 信息安全 | 90.5
5 | 李眉 | 软件工程 | 87.0
查询结果集表格(以findAll为例)
| stu_id | stu_name | stu_age | stu_major | stu_score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
分析
<id>与<result>的区别:<id>用于标识主键列,MyBatis会利用它进行结果集去重和缓存键生成;<result>用于普通列。在简单查询中二者功能差异不明显,但在关联查询和缓存场景下,<id>的正确配置至关重要。extends继承:FullResultMap通过extends="BaseResultMap"复用了基础映射规则,只需补充新增字段。这在大型项目中极为实用:基础字段统一维护,扩展映射按需叠加。autoMapping的隐性作用:即使FullResultMap没有显式声明stuId、stuName、stuAge的映射(因为它们在BaseResultMap中),继承后这些规则依然生效。若关闭autoMapping且未显式声明某列,该列将被忽略。
易错场景/常见误区
| 误区 | 正解 |
|---|---|
resultMap的id属性与<id>子元素混淆 | resultMap的id是整个映射规则的标识;<id>子元素是主键列的映射声明 |
认为extends会覆盖父resultMap的同名映射 | extends是追加而非覆盖;子resultMap中重新定义同名result会报错或产生歧义 |
在resultMap中遗漏<id>,全部用<result> | 主键应使用<id>声明,这有助于MyBatis优化缓存和结果集处理 |
column写Java属性名,property写数据库列名 | column对应数据库列名,property对应Java属性名,二者不可写反 |
认为resultMap比resultType性能差 | 二者性能差异微乎其微;resultMap的显式映射在复杂场景下反而更可控 |
关闭autoMapping后期望未声明列自动映射 | autoMapping="false"时,只有resultMap中显式声明的列才会被映射,其余列被忽略 |
面试考点
Q1:resultMap中的<id>和<result>有什么区别?
<id>用于标识主键列与属性的映射,MyBatis用它进行结果集唯一性判断、缓存键生成和对象复用。<result>用于普通列的映射。在简单查询中二者表现相似,但在嵌套结果映射和二级缓存场景下,<id>的正确配置直接影响结果正确性。
Q2:resultMap的extends有什么作用?
extends允许一个resultMap继承另一个resultMap的所有映射规则,在此基础上追加或覆盖(不建议覆盖)新的映射。这实现了映射规则的复用,避免在多个resultMap中重复书写相同的<id>和<result>配置,提升维护性。
Q3:autoMapping在resultMap中如何工作?
当
autoMapping开启(默认或显式设置true)时,MyBatis会先处理resultMap中显式声明的<id>和<result>,然后对剩余未显式声明但列名与属性名匹配的列进行自动映射。若设置为false,则只映射显式声明的列,其余忽略。
Q4:什么情况下必须用resultMap而不能用resultType?
以下情况必须使用
resultMap:数据库列名与Java属性名不一致且无法通过驼峰转换匹配;需要进行复杂的类型转换(如将VARCHAR状态码映射为枚举);需要嵌套关联映射(一对一、一对多);需要控制自动映射行为。简单的一致命名场景优先使用resultType以减少配置。
小结
resultMap是MyBatis结果映射的终极解决方案。通过<id>标识主键、<result>声明普通列映射,它彻底解决了列名与属性名不一致的难题。extends继承机制让映射规则可以分层复用,避免重复配置。autoMapping则提供了显式映射与自动映射的混合策略。掌握resultMap后,任何复杂的结果集形态都能被精确映射到Java对象中。
下一章引子
SQL语句写好了,结果映射也配置完毕,但参数是如何从Java对象传递到SQL中的?#{}和${}两种占位符看起来相似,实则天壤之别:一个安全预编译,一个危险字符串替换。下一节将深入讲解参数传递机制,通过流程图对比二者的底层差异,并现场演示SQL注入攻击,帮助你建立牢固的安全意识。