9.4.5 ResultMaps

9.4.5 ResultMaps

resultMap元素是MyBatis中最重要最强大的元素。它的作用是告诉MyBatis将从结果集中取出的数据转换成开发者所需要的对象。

查询结果的每条记录转换成一个Map

下面是最简单的映射语句示例:

1
2
3
<select id="selectUser" resultType="map">
select * from tb_user
</select>
  • selectUserselect元素执行一条查询语句,查询tb_user表的所有数据。
  • resultType="map"表示返回的每条数据是都是一个Map集合(使用这条记录的列名作为key,列值作为value)。

示例: 测试ResultMaps

项目结构

展开/折叠
G:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\09\ResultMapTest
├─src\
│ ├─db.properties
│ ├─log4j.xml
│ ├─mybatis-config.xml
│ └─org\
│   └─fkit\
│     ├─domain\
│     │ ├─Clazz.java
│     │ ├─Student.java
│     │ └─User.java
│     ├─factory\
│     │ └─FKSqlSessionFactory.java
│     ├─mapper\
│     │ └─UserMapper.xml
│     └─test\
│       ├─ResultMapTest.java
│       ├─SelectClazzTest.java
│       ├─SelectMapTest.java
│       └─SelectStudentTest.java
├─tb_user2.sql
└─WebContent\
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─lib\
    │ ├─ant-1.9.6.jar
    │ ├─ant-launcher-1.9.6.jar
    │ ├─asm-5.2.jar
    │ ├─cglib-3.2.5.jar
    │ ├─commons-logging-1.2.jar
    │ ├─javassist-3.22.0-CR2.jar
    │ ├─log4j-1.2.17.jar
    │ ├─log4j-api-2.3.jar
    │ ├─log4j-core-2.3.jar
    │ ├─mybatis-3.4.5.jar
    │ ├─mysql-connector-java-8.0.17.jar
    │ ├─ognl-3.1.15.jar
    │ ├─slf4j-api-1.7.25.jar
    │ └─slf4j-log4j12-1.7.25.jar
    └─web.xml

resultType为map

SelectMapTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package org.fkit.test;

import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.SqlSession;
import org.fkit.factory.FKSqlSessionFactory;

public class SelectMapTest
{

public static void main(String[] args)
{
// 1.定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 2.创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
// 3.查询TB_USER表所有记录,每一条记录都封装Map,
// 该记录的属性作为key,属性值作为value
List<Map<String, Object>> list = sqlSession.selectList("org.fkit.mapper.UserMapper.selectUser");
// 遍历List集合,打印每一个Map对象
list.forEach(row -> System.out.println(row));
// 4.提交事务
sqlSession.commit();
} catch (Exception e)
{
// 5.回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}
}
}

对应的select标签

1
2
3
4
5
6
7
<!-- select操作
resultType="map"返回的每条记录都封装成一个Map
对于每条记录,使用这条记录的列名做key,值做value -->
<select
id="selectUser"
resultType="map"> SELECT * FROM TB_USER
</select>

数据库表

此时tb_user表中的数据如下:

1
2
3
4
5
6
7
8
9
10
mysql> select * from tb_user;
+----+------+-----+-----+
| id | name | sex | age |
+----+------+-----+-----+
| 2 | 小丽 | 男 | 22 |
| 3 | 小李 | 男 | 22 |
| 4 | 小王 | 女 | 20 |
| 5 | 小明 | 女 | 20 |
+----+------+-----+-----+
4 rows in set

运行结果

运行SelectMapTest类的main方法,控制台显示如下:

1
2
3
4
5
6
7
DEBUG [main] ==>  Preparing: SELECT * FROM TB_USER 
DEBUG [main] ==> Parameters:
DEBUG [main] <== Total: 4
{sex=男, name=小丽, id=2, age=22}
{sex=男, name=小李, id=3, age=22}
{sex=女, name=小王, id=4, age=20}
{sex=女, name=小明, id=5, age=20}

可以看到,査询语句返回的每一条记录都被封装成一个Map集合,这条记录的列名作为Map集合的key,而列的值作为Mapvalue.

resultType为JavaBean

虽然数据被封装成Map集合返回,但是Map集合并不能很好地描述一个领域模型。在实际项目开发中更加建议使用JavaBeanPOJO(Plain Old Java Object,普通Java对象)作为领域模型描述数据。例如:

1
2
3
<select id="selectUser" resultType="org.fkit.domain.User">
select * from tb_user
</select>

数据表列名和持久化对象属性名不一致的情况 使用resultMap标签

默认情况下,MyBatis会将査询到的记录的列和需要返回的对象(User)的属性逐一进行匹配赋值,但是,如果查询到的记录的列名和需要返回的对象(User)的属性名不一致,则MyBatis就不会自动赋值了,这时,可以使用ResultMap标签进行处理.

创建数据库表tb_user2

进入mybatis数据库,创建一个表tb_user2,并插入几条测试数据:
cmd命令行中输入下面的sql语句进行创建,注意是在cmd命令行,如果使用Navicat的话先保存成sql文件,然后通过导入SQL文件的方式创建,不要使用Navicat的命令行界面,因为直接复制粘贴下面代码插入数据的时候,会出现乱码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set names "gbk";
use mybatis;
DROP TABLE IF EXISTS `tb_user2`;
CREATE TABLE `tb_user2` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(18) NOT NULL,
`user_sex` varchar(18) NOT NULL,
`user_age` int(11) NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO tb_user2 VALUES (null, '小明', '男', 24);
INSERT INTO tb_user2 VALUES (null, '小王', '男', 25);
INSERT INTO tb_user2 VALUES (null, '小丽', '女', 22);
INSERT INTO tb_user2 VALUES (null, '小花', '女', 25);

创建持久化对象User.java

接下来创建一个User对象映射tb_user2表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id;
private String name;
private String sex;
private Integer age;
public User()
{
super();
// TODO Auto-generated constructor stub
}
public User(String name, String sex, Integer age)
{
super();
this.name = name;
this.sex = sex;
this.age = age;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
}

创建resultMap标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- 映射数据库表的列名和持久化对象的属性名 -->
<!-- 用在两者不同名的情况 -->
<resultMap
id="userResultMap"
type="org.fkit.domain.User">
<!-- select语句结果集的user_id字段值映射到持久化对象User的id成员变量上 -->
<id
column="user_id"
property="id" />
<!-- 结果集的user_name字段的值,映射到User对象的成员变量name上 -->
<result
column="user_name"
property="name" />
<!-- 结果集的user_sex字段映射User对象的成员变量sex -->
<result
column="user_sex"
property="sex" />
<result
column="user_age"
property="age" />
</resultMap>

<!-- 使用自定义的映射 -->
<select
id="selectUser2"
resultMap="userResultMap">
SELECT * FROM TB_USER2
</select>

resultMap标签的属性

上面使用了一个新的元素resultMap,该元素常用属性如下

  • resultMap元素的id属性,是resultMap的唯一标识符。
  • resultMap元素的type属性,表示resultMap实际返回的类型。

上面还使用了resultMap的两个子元素idresult:

resultMap标签的id子标签
  • id,表示数据库表的主键列,其中,
    • column属性表示数据库表的列名,
    • property表示数据库列映射到返回类型的属性名
resultMap标签的result子标签
  • result,表示数据库表的普通列,其中,
    • column属性表示数据库表的列名,
    • property表示这个数据库列名要映射到的持久化对象的属性名

ResultMapTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ResultMapTest {
public static void main(String[] args)
{
// 定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 1.创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
//
List<User> user_list = sqlSession.selectList("org.fkit.mapper.UserMapper.selectUser2");
// 遍历List集合,打印每一个Map对象
user_list.forEach(user -> System.out.println(user));
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}
}
}

运行ResultMapTest类的main方法,控制台显示如下:

1
2
3
4
5
6
7
DEBUG [main] ==>  Preparing: SELECT * FROM TB_USER2 
DEBUG [main] ==> Parameters:
DEBUG [main] <== Total: 4
User [id=1, name=小明, sex=男, age=24]
User [id=2, name=小王, sex=男, age=25]
User [id=3, name=小丽, sex=女, age=22]
User [id=4, name=小花, sex=女, age=25]

可以看到,虽然表TB_USER2的列名和User对象的属性名不一致,但是经过resultMap映射后,数据依然被正确封装到User对象当中

关联映射

在实际项目开发中,还有更加复杂的情况,例如执行的是一个多表查询语句,而返回的对象关联到另一个对象,此时简单地映射已经无法解决问题,必须使用resultMap元素来完成关联映射

创建两个表格 tb_cazz和tb_student

进入mybatis数据库,创建两个表tb_cazztb_student,并分别插入几条测试数据:

1
2
3
4
5
6
7
8
# 如果存在 tb clazz表则删除
drop table if exists tb_clazz;
create table tb_clazz(
id int primary key auto_increment,
code varchar(18)
);
insert into tb_clazz(code) values ('j1601');
insert into tb_clazz(code) values('j1602');
1
2
3
4
5
6
7
8
9
10
11
12
13
# 如果存在tb_student表则删除
create table tb_student(
id int primary key auto_increment,
name varchar(18),
sex char(3),
age int,
clazz_id int,
foreign key(clazz_id) references tb_clazz(id)
);
insert into tb_student(name, sex, age, clazz_id) values ('jack','男', 22, 1);
insert into tb_student(name, sex, age, clazz_id) values ('rose','女', 18, 1);
insert into tb_student(name, sex, age, clazz_id) values ('tom','男', 25, 2);
insert into tb_student(name, sex, age, clazz_id) values ('mary','女',20,2);

以上SQL语句插入了两个班级记录和4个学生记录,两个学生分配在1班,两个学生分配在2班。需要指出的是,tb_student表中的clazz_id列作为外键引用tb_clazz表的id列,表示学生对应的班级。

接下来我们要做的是:
查询出所有的学生信息,同时关联查询出学生对应的班级信息。

创建持久化对象

创建一个Clazz对象和Student对象并分别映射tb_clazz表和tb_student表。

Clazz.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Clazz
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id;
private String code;
private List<Student> students;
public Clazz()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
public List<Student> getStudents()
{
return students;
}
public void setStudents(List<Student> students)
{
this.students = students;
}
@Override
public String toString()
{
return "Clazz [id=" + id + ", code=" + code + "]";
}
}

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Student
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id;
private String name;
private String sex;
private Integer age;
// 关联的Clazz对象
private Clazz clazz;
public Student()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Student [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + ", clazz="
+ clazz + "]";
}
}

需要注意的是,Student中的属性clazzClazz类的一个对象,clazz对象的的属性有idcode。这是现代开发中最常用的对象关联方式。

映射配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- ############################### 关联映射示例 ####################################### -->

<!-- 查询所有学生信息 -->
<select
id="selectStudent"
resultMap="studentResultMap">
SELECT * FROM TB_STUDENT
</select>

<!-- 映射学生对象的resultMap -->
<resultMap
id="studentResultMap"
type="org.fkit.domain.Student">
<!-- 结果集的id字段映射Student类id成员变量 -->
<id
column="id"
property="id" />
<!-- 结果集name字段映射Student类的name成员变量 -->
<result
column="name"
property="name" />
<!-- 结果集sex字段映射Student类的sex成员变量 -->
<result
column="sex"
property="sex" />
<!-- 结果集的age字段映射Student类的age成员变量 -->
<result
column="age"
property="age" />
<!-- 关联映射 执行子查询 -->
<!-- column="clazz_id"表示使用结果集中的clazz_id字段作为子查询语句的参数 -->
<!-- select="selectClazzWithId"表示子查询的位置 -->
<!-- property="clazz"表示 接收子查询结果的成员变量为:Student类的clazz -->
<!-- javaType="org.fkit.domain.Clazz"表示接收子查询结果的成员变量clazz的类型 -->
<association
column="clazz_id"
select="selectClazzWithId"
property="clazz"
javaType="org.fkit.domain.Clazz" />
</resultMap>
<!-- 根据 班级id 查询班级 -->
<select
id="selectClazzWithId"
resultType="org.fkit.domain.Clazz">
SELECT * FROM TB_CLAZZ where id = #{id}
</select>

上面的映射相对之前复杂了一些,具体解释如下:
①首先执行idselectStudentselect元素,查询所有的学生数据,此时返回的不是简单的Student对象,因为Student对象中还包含了Clazz对象,所以使用resultMap去映射返回类型。
idstudentResultMapresultMap元素返回类型为org.fkit.domain.Student,其中,idnamesexage都是简单的属性映射,而查询的班级idclazz则使用了关联映射association

association元素属性解释

association元素的解释如下

  • column。(子查询的参数)表示执行select语句返回的结果集中表的列名
  • select。(子查询的位置)表示执行一条査询语句,将查询到的记录封装到property所代表的类型对象当中。上面的selectClazzWithId执行一条SQL语句,将学生的clazz_id作为参数查询对应的班级信息
  • property。(接收子查询结果的成员变量)表示返回类型Student的属性名clazz
  • javaType。(接收子查询结果的成员变量的类型)表示该clazz属性对应的类型名称,本示例是一个Clazz类型。

SelectStudentTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.fkit.test;

import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.fkit.domain.Student;
import org.fkit.factory.FKSqlSessionFactory;

public class SelectStudentTest
{

public static void main(String[] args)
{
// 定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
// 查询TB_USER表所有数据返回List集合,集合中的每个元素都是一个Student对象
List<Student> student_list = sqlSession.selectList("org.fkit.mapper.UserMapper.selectStudent");
// 遍历List集合,打印每一个Student对象,该对象包含关联的Clazz对象
student_list.forEach(stu -> System.out.println(stu));
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}
}
}

运行结果

运行SelectStudentTest类的main方法,控制台显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
DEBUG [main] ==>  Preparing: SELECT * FROM TB_STUDENT 
DEBUG [main] ==> Parameters:
DEBUG [main] ====> Preparing: SELECT * FROM TB_CLAZZ where id = ?
DEBUG [main] ====> Parameters: 1(Integer)
DEBUG [main] <==== Total: 1
DEBUG [main] ====> Preparing: SELECT * FROM TB_CLAZZ where id = ?
DEBUG [main] ====> Parameters: 2(Integer)
DEBUG [main] <==== Total: 1
DEBUG [main] <== Total: 4
Student [id=1, name=小明, sex=男, age=22, clazz=Clazz [id=1, code=B180102]]
Student [id=2, name=小王, sex=男, age=22, clazz=Clazz [id=2, code=B180203]]
Student [id=3, name=小丽, sex=男, age=21, clazz=Clazz [id=2, code=B180203]]
Student [id=4, name=小芳, sex=男, age=23, clazz=Clazz [id=1, code=B180102]]

可以看到,因为使用了关联映射,查询学生信息时学生对应的班级对象也被查询出来了。

集合映射

现在査询所有学生时可以关联查询出班级信息了,那如果反过来,查询所有班级时需要查询出班级中的所有学生对象,应该如何映射呢?
学生通常只对应一个班级,但是班级中会有多个学生存在,所以首先在Clazz.java类中增加一个字段students,该字段是一个List集合,表示班级的多个学生。

1
2
3
4
5
6
7
public class Clazz
implements Serializable
{
//......
private List<Student> students;
//......
}

映射配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!-- ###################################### 集合映射示例 ############################################ -->
<!-- 查询 所有班级信息 -->
<select
id="selectClazz"
resultMap="clazzResultMap">
SELECT * FROM TB_CLAZZ
</select>
<!-- 映射班级对象的resultMap -->
<resultMap
id="clazzResultMap"
type="org.fkit.domain.Clazz">
<!-- 简单映射 -->
<!-- select结果集id字段值 映射到 Clazz对象id成员变量 -->
<id
column="id"

property="id" />
<!-- 简单映射 -->
<!-- select结果集code字段值 映射到 Clazz对象code成员变量 -->
<result
column="code"
property="code" />
<!-- 班级的学生属性,因为一个班级有多个学生,所以该属性是一个集合 -->
<!-- 使用结果集中的id字段作为子查询的参数 -->
<!-- select表示子查询的位置 -->
<!-- property表示接收子查询结果的成员变量 -->
<!-- javaType表示接收子查询的成员变量的类型(集合) -->
<!-- 由于接收子查询结果的成员变量是一个集合,ofType集合中保存的是什么类型 -->
<collection
column="id"
select="selectStudentWithId"
property="students"
javaType="ArrayList"
ofType="org.fkit.domain.Student" />
</resultMap>

<!-- 根据 班级id 查询学生 -->
<select
id="selectStudentWithId"
resultType="org.fkit.domain.Student">
SELECT * FROM TB_STUDENT where clazz_id = #{id}
</select>

上面的映射和查询学生关联班级类似,具体解释如下:
①首先执行idselectClazzselect元素,查询所有的班级数据,此时返回的不是简单的Clazz对象,因为Clazz对象中还包含了学生的集合对象,所以使用resultMap去映射返回类型。
idclazzResultMap素返回类型为org.fkit.domain.Clazz
其中,idcode都是简单的属性映射,而査询班级所有学生时则使用了集合映射collection

collection标签详解

collection元素的解释如下:

  • column。(子查询输入参数)表示使用select语句结果集中的id字段作为参数进行之后的select语句查询。
  • select。(子查询语句位置)表示执行一条查询语句,将查询到的数据封装到property所代表的类型对象当中。上面的selectStudentWithId执行一条SQL语句,将班级的id作为参数查询班级对应的所有学生信息。
  • property。(接收子查询结果的成员变量)表示返回类型Clazz的属性名students
  • javaType。(接收子查询结果的成员变量的类型)表示该属性对应的类型名称,本示例中是一个Arraylist集合。
  • ofType。(接收子查询结果的集合中的元素的类型)表示集合当中的类型,本示例中是Student类型。

SelectClazzTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.fkit.test;

import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.fkit.domain.Clazz;
import org.fkit.domain.Student;
import org.fkit.factory.FKSqlSessionFactory;

public class SelectClazzTest
{

public static void main(String[] args)
{
// 定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
// 查询TB_CLAZZ表所有数据返回List集合,集合中的每个元素都是一个Clazz对象
List<Clazz> clazz_list = sqlSession.selectList("org.fkit.mapper.UserMapper.selectClazz");
// 遍历List集合,打印每一个Clazz对象和该Clazz关联的所有Student对象
for (Clazz clazz : clazz_list)
{
System.out.println(clazz + ":");
List<Student> student_list = clazz.getStudents();
for (Student stu : student_list)
{
System.out.println(" " + stu.getId() + " " +
stu.getName() + " " + stu.getSex() + " " + stu.getAge());
}
}
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}

}
}

运行结果

运行SelectClazzTest类的main方法,控制台显示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DEBUG [main] ==>  Preparing: SELECT * FROM TB_CLAZZ 
DEBUG [main] ==> Parameters:
DEBUG [main] ====> Preparing: SELECT * FROM TB_STUDENT where clazz_id = ?
DEBUG [main] ====> Parameters: 1(Integer)
DEBUG [main] <==== Total: 2
DEBUG [main] ====> Preparing: SELECT * FROM TB_STUDENT where clazz_id = ?
DEBUG [main] ====> Parameters: 2(Integer)
DEBUG [main] <==== Total: 2
DEBUG [main] <== Total: 2
Clazz [id=1, code=j1601]:
1 jack 男 22
2 rose 女 18
Clazz [id=2, code=j1602]:
3 tom 男 25
4 mary 女 20

可以看到,因为使用了集合映射,所以査询班级信息时班级对应的所有学生对象也被查询出来了。