MyBatis笔记(3)-自定义映射resultMap

简介

在前面的select标签当中,我们提到一定要指定resultType或者resultMap。前面我们一直使用的都是resultType,接下来就来介绍resultMap的使用。

无论是resultType还是resultMap,它们完成的工作都是确定如何将查询得到的一行数据与Java中的实体类完成映射。在前面的环境中,我们数据库中的users表有如下的表结构:

id username password age gender email

而我们的实体类User中的定义如下:

1
2
3
4
5
6
7
8
9
10
11
public class User {
private int id;
private String username;
private String password;
private int age;
private String gender;
private String email;

// ...
// 省略了构造方法、get set方法、toString方法等
}

可以看到的是,表中的字段和实体类中的属性是可以对应上的。(注意这里关于成员变量以及属性的区分)默认属性名和字段名保持一致的话,可以完成映射,因此我们前面一直使用的都是resultType直接指定实体类。而resultMap为自定义映射,解决的是一些属性和字段没有保持一致,或者一些多对一,一对多的映射情况。接下来就进行相关的说明。

这里说明后续的数据库环境。在数据库中新建了两张表,分别是员工表以及部门表,其中员工表中的字段dept_id表示员工所处的部门,部门表的主键。

员工表emp中的字段如下,同时准备了一些测试数据:

emp_id emp_name dept_id
1 1
2 2
3 2
4 1

部门表dept中的字段如下:

dept_id dept_name
1 A
2 B

resultMap使用

字段和属性映射

在Java中,我们一般遵循的是驼峰命名法,而在数据库中,我们一般使用下划线。这样就可能出现字段和属性无法对应的问题。

例如现在我们需要查询员工中的数据,我们准备了一个员工的实体类如下,其中的命名遵循的是驼峰命名法。

1
2
3
4
5
6
7
8
public class Emp {
private int empId;
private String empName;
private int deptId;

// ...
// 省略相关方法
}

之后我们写一个简单的接口,按照员工id来查询该员工的信息。

1
2
3
4
5
6
<!--Emp getEmpById(@Param("id") int empId)-->
<select id="getEmpById" resultType="com.syh.bean.Emp">
select *
from emp
where emp_id = #{id}
</select>

执行测试方法,发现输出为null。但是实际上我们能够查询到数据的,这实际上就是因为表中的字段名和实体类的属性名没有对应上,我们查出来的一行,它的字段名是emp_id, emp_name, dept_id,而我们的属性名则是empId, empName, deptId,无法对应,因此查出为null。

解决这个问题有两种方式,第一种是在核心配置文件中开启下划线到驼峰的转换设置,如下所示,这样就可以开启下划线方式与驼峰方式命名之间的对应,二者能够对应上了,也就可以查出结果来了。

1
2
3
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

第二种方式则是使用resultMap。我们在映射文件中先定义一个resultMap,然后在其中指定属性与字段的对应关系,如下所示。最后在select标签中指定使用对应的resultMap,这样也可以查出结果。这种解决方案同样是给出了对应关系,告诉MyBatis该如何完成对应,因此可以查出结果。

1
2
3
4
5
6
7
8
9
10
11
12
<resultMap id="empMap" type="com.syh.bean.Emp">
<id property="empId" column="emp_id"/>
<result property="empName" column="emp_name"/>
<result property="deptId" column="dept_id"/>
</resultMap>

<!--Emp getEmpById(@Param("id") int empId)-->
<select id="getEmpById" resultMap="empMap">
select *
from emp
where emp_id = #{id}
</select>

在resultMap标签中,使用了id属性以及type属性:

  • id:表示自定义映射的唯一标识
  • type:表示查询的数据需要映射到的实体类的类型

在子标签中,我们使用到了id标签以及result标签,还有property,column属性:

  • id 标签:设置主键的对应关系
  • result 标签:设置普通字段的映射关系
  • property 属性:设置映射关系中实体类的属性名
  • column 属性:设置映射关系中表的字段名

后面我们还会用到association标签以及collection标签,分别解决多对一和一对多的映射关系。

多对一映射处理

多对一的关系指的是数据库中表的关系。例如在我们目前的环境中,多个员工对应一个部门,就是一种多对一的关系。我们稍微修改一下实体类的设计,使其能够对应我们的关系。

修改后的Emp类:

1
2
3
4
5
6
7
8
public class Emp {
private int empId;
private String empName;
private Dept dept; // 一个员工对应一个部门

// ...
// 省略相关方法
}

修改后的Dept类:

1
2
3
4
5
6
7
8
public class Dept {
private int deptId;
private String deptName;
private List<Emp> empList; // 一个部门对应多个员工

// ...
// 省略相关方法
}

我们目前希望完成的查询同样是查询一个员工的信息,可以写出如下的查询语句(这里开启了全局的下划线转驼峰的设置):

1
2
3
4
5
6
7
<!--Emp getEmpById(@Param("id") int empId)-->
<select id="getEmpById" resultType="com.syh.bean.Emp">
select *
from emp
left join dept on emp.dept_id = dept.dept_id
where emp.emp_id = #{id}
</select>

但是经过测试输出后,我们发现输出结果如下:

1
Emp{empId=1, empName='甲', dept=null}

其中emp相关的属性确实正确的返回了,但是部门信息没有返回。分析原因,同样是MyBatis无法根据现有的信息找到对应关系,查询结果中的字段分别为emp_id, emp_name, emp.dept_id, dept.dept_id, dept_name,能够对应的字段只有emp_id和emp_name,所以也就只有这两个属性能够对应上。

解决多对一映射问题的方法有三种,分别是级联方式,association标签以及分步查询,下面分别进行介绍。

级联方式

使用级联方式,就是让我们在resultMap中指定相关的对应关系。如果存在类之间的级联,使用.来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resultMap id="empMap" type="com.syh.bean.Emp">
<id property="empId" column="emp_id"/>
<result property="empName" column="emp_name"/>
<result property="dept.deptId" column="dept_id"/>
<result property="dept.deptName" column="dept_name"/>
</resultMap>

<!--Emp getEmpById(@Param("id") int empId)-->
<select id="getEmpById" resultMap="empMap">
select *
from emp
left join dept on emp.dept_id = dept.dept_id
where emp.emp_id = #{id}
</select>

查出结果如下,发现可以查出对应的部门信息:

1
Emp{empId=1, empName='甲', dept=Dept{deptId=1, deptName='A', empList=null}}

association

第二种方式是使用resultMap下的子标签association,在其中指定对应的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<resultMap id="empMap" type="com.syh.bean.Emp">
<id property="empId" column="emp_id"/>
<result property="empName" column="emp_name"/>
<association property="dept" javaType="com.syh.bean.Dept">
<id property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>

<!--Emp getEmpById(@Param("id") int empId)-->
<select id="getEmpById" resultMap="empMap">
select *
from emp
left join dept on emp.dept_id = dept.dept_id
where emp.emp_id = #{id}
</select>

association标签是用来设置多对一的映射关系的,其中的property表示需要映射的实体类中的属性,javaType表示要映射成为哪个类。在association子标签中,同样有id以及result子标签,表示主键以及普通字段。

分步查询

分步查询则将这个问题分成两个步骤来进行解决。这个问题可以拆解成两步完成,第一步先查出员工的部门id,第二步则是根据员工所对应的部门id查询部门信息。

首先第一步,查询员工的信息:

1
2
3
4
5
6
7
8
9
10
11
12
<resultMap id="empMapStep" type="com.syh.bean.Emp">
<id property="empId" column="emp_id"/>
<result property="empName" column="emp_name"/>
<association property="dept" select="com.syh.mapper.DeptMapper.getDeptById" column="dept_id"/>
</resultMap>

<!--Emp getEmpByIdStep(@Param("id") int empId)-->
<select id="getEmpByIdStep" resultMap="empMapStep">
select *
from emp
where emp_id = #{id}
</select>

这里,association中设置的select表示下一步需要指定的操作,column表示提供给它的参数。这里下一步操作即调用对应的接口,这里的操作应该是根据部门id来查询部门的相关信息,因此接下来就实现第二步操作。

1
2
3
4
5
6
<!--Dept getDeptById(@Param("id") int dept_id)-->
<select id="getDeptById" resultType="com.syh.bean.Dept">
select *
from dept
where dept_id = #{id}
</select>

最终查询的输出如下,可以看到是能够正常查询得到结果的,并且是进行了分步的查询:

1
2
3
4
5
6
7
DEBUG 10-11 20:54:39,378 ==>  Preparing: select * from emp where emp_id = ? (BaseJdbcLogger.java:137) 
DEBUG 10-11 20:54:39,414 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 10-11 20:54:39,441 ====> Preparing: select * from dept where dept_id = ? (BaseJdbcLogger.java:137)
DEBUG 10-11 20:54:39,442 ====> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 10-11 20:54:39,444 <==== Total: 1 (BaseJdbcLogger.java:137)
DEBUG 10-11 20:54:39,446 <== Total: 1 (BaseJdbcLogger.java:137)
Emp{empId=1, empName='甲', dept=Dept{deptId=1, deptName='A', empList=null}}

一对多映射处理

上面的情况中,多个员工对应一个部门是多对一的情况,那么反过来就是一对多的情况。反映的需求当中,现在我们希望实现根据部门id来查询部门以及部门中员工的信息。在Dept实体类中,存在一个Emp列表,直接查询是无法得到结果的,因此还是需要使用自定义映射来解决。解决问题的方式有两种,分别是使用collection标签和分步查询。

collection

使用resultMap中的collection标签可以解决一对多映射处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<resultMap id="deptMap" type="com.syh.bean.Dept">
<id property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<collection property="empList" ofType="com.syh.bean.Emp">
<id property="empId" column="emp_id"/>
<result property="empName" column="emp_name"/>
</collection>
</resultMap>

<!--Dept getDeptEmpById(@Param("id") int dept_id)-->
<select id="getDeptEmpById" resultMap="deptMap">
select *
from dept
join emp on dept.dept_id = emp.dept_id
where dept.dept_id = #{id}
</select>

在collection标签中,我们需要指定property属性,以及集合中元素的类型ofType,内部子标签id以及result,则同样代表了主键以及普通字段的映射。

测试输出结果如下,发现可以正常得到结果。

1
Dept{deptId=1, deptName='A', empList=[Emp{empId=1, empName='甲', dept=null}, Emp{empId=4, empName='丁', dept=null}]}

其中每个Emp的部门显示为null也是因为我们没有在collection中指定对应关系。

分步查询

第二种方式是分步查询。这个问题也可以拆分成两个步骤,第一步查询出部门的相关信息,得到部门id;第二步根据部门id来查询员工信息。

1
2
3
4
5
6
7
8
9
10
11
12
<resultMap id="deptMapStep" type="com.syh.bean.Dept">
<id property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<collection property="empList" select="com.syh.mapper.EmpMapper.getEmpByDeptId" column="dept_id"/>
</resultMap>

<!--Dept getDeptEmpByIdStep(@Param("id") int dept_id)-->
<select id="getDeptEmpByIdStep" resultMap="deptMapStep">
select *
from dept
where dept_id = #{id}
</select>

这里在collection标签中,同样需要指定select表示下一步的操作,column表示传递过去的参数。下一步是根据部门id来查询员工信息,则可以用下面的操作来完成:

1
2
3
4
5
6
<!--List<Emp> getEmpByDeptId(@Param("id") int deptId)-->
<select id="getEmpByDeptId" resultType="com.syh.bean.Emp">
select *
from emp
where dept_id = #{id}
</select>

(这里的方法返回类型,定义成List<Emp>还是Emp,都能够得到正确的结果,不过按照场景,定义成列表更加符合逻辑)

之后进行测试,可以得到下面的输出结果:

1
2
3
4
5
6
7
DEBUG 10-11 21:22:01,439 ==>  Preparing: select * from dept where dept_id = ? (BaseJdbcLogger.java:137) 
DEBUG 10-11 21:22:01,474 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 10-11 21:22:01,498 ====> Preparing: select * from emp where dept_id = ? (BaseJdbcLogger.java:137)
DEBUG 10-11 21:22:01,499 ====> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 10-11 21:22:01,502 <==== Total: 2 (BaseJdbcLogger.java:137)
DEBUG 10-11 21:22:01,504 <== Total: 1 (BaseJdbcLogger.java:137)
Dept{deptId=1, deptName='A', empList=[Emp{empId=1, empName='甲', dept=null}, Emp{empId=4, empName='丁', dept=null}]}

可以发现确实查询到了正确的结果,并且使用了分步查询。

延迟加载

在多对一和一对多的映射处理中,我们都使用到了分步查询来解决。分步查询的优点在于可以实现延迟加载。延迟加载指的是可以完成按需加载,当前获取的数据是什么,就执行相应的SQL,而不需要全部执行。

开启延迟加载需要在核心配置文件中开启全局配置信息:

1
2
3
4
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
  • lazyLoadingEnabled:延迟加载的全局配置。当设置为true时,所有的关联对象都会延迟加载
  • aggressiveLazyLoading:加载属性的策略。当设置为true的时候,任何方法的调用都会加载该对象的所有属性。否则按需加载。默认值为false

如果测试代码中,我们只访问部门的id,而不需要访问部门的员工列表,则它会进行按需加载。测试代码和输出分别如下,可以看到其中只访问了部门的id,只执行了第一步的SQL语句。

关键测试代码:

1
2
3
DeptMapper mapper = sqlSession.getMapper(DeptMapper.class);
Dept deptEmpById = mapper.getDeptEmpByIdStep(1);
System.out.println(deptEmpById.getDeptName());

输出:

1
2
3
4
DEBUG 10-11 21:29:25,743 ==>  Preparing: select * from dept where dept_id = ? (BaseJdbcLogger.java:137) 
DEBUG 10-11 21:29:25,772 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 10-11 21:29:25,841 <== Total: 1 (BaseJdbcLogger.java:137)
A

上面的配置是全局配置信息,对全局生效。实际上每个collection或者association标签也有自己的按需加载的开关,即标签中的fetchType属性。该属性可选的取值有两种,分别是lazy延迟加载,以及eager立即加载,默认取值为lazy


MyBatis笔记(3)-自定义映射resultMap
http://example.com/2022/10/11/MyBatis笔记-3-自定义映射resultMap/
作者
EverNorif
发布于
2022年10月11日
许可协议