MyBatis核心之进阶开发

MyBatis核心之进阶开发

一、MyBatis开发环境介绍

本次实践案例选择使用两个关联表t_empt_dept,这两个表的关系是多对一即一个部门中有多个员工,每个员工只能对应一个部门:

image-20231124182027531

对应的两个实体类是EmployeeDepartment,具体内容如下:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.huling.pojo;

public class Employee {
private Integer eid;
private String empName;
private Integer empAge;
private String empSex;
private String empEmail;
private Department dept;

public Employee() {
}

public Employee(Integer eid, String empName, Integer empAge, String empSex, String empEmail) {
this.eid = eid;
this.empName = empName;
this.empAge = empAge;
this.empSex = empSex;
this.empEmail = empEmail;
}

public Integer getEid() {
return eid;
}

public void setEid(Integer eid) {
this.eid = eid;
}

public String getEmpName() {
return empName;
}

public void setEmpName(String empName) {
this.empName = empName;
}

public Integer getEmpAge() {
return empAge;
}

public void setEmpAge(Integer empAge) {
this.empAge = empAge;
}

public String getEmpSex() {
return empSex;
}

public void setEmpSex(String empSex) {
this.empSex = empSex;
}

public String getEmpEmail() {
return empEmail;
}

public void setEmpEmail(String empEmail) {
this.empEmail = empEmail;
}

public Department getDept() {
return dept;
}

public void setDept(Department dept) {
this.dept = dept;
}

@Override
public String toString() {
return "Employee{" +
"eid=" + eid +
", empName='" + empName + '\'' +
", empAge=" + empAge +
", empSex='" + empSex + '\'' +
", empEmail='" + empEmail + '\'' +
", dept=" + dept +
'}';
}
}
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
49
package com.huling.pojo;

import java.util.List;

public class Department {
private Integer did;
private String deptName;
private List<Employee> employees;
public Department() {
}

public Department(Integer did, String deptName) {
this.did = did;
this.deptName = deptName;
}

public Integer getDid() {
return did;
}

public void setDid(Integer did) {
this.did = did;
}

public String getDeptName() {
return deptName;
}

public void setDeptName(String deptName) {
this.deptName = deptName;
}

public List<Employee> getEmployees() {
return employees;
}

public void setEmployees(List<Employee> employees) {
this.employees = employees;
}

@Override
public String toString() {
return "Department{" +
"did=" + did +
", deptName='" + deptName + '\'' +
", employees=" + employees +
'}';
}
}

项目的整体结构图如下:

image-20231124182318175

我这里开启了配置选项mapUnderscoreToCamelCase,因此除了Employee类中的dept和Department类中的employees其他字段都能自动映射。

二、MyBatis的结果映射

1.字段映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// EmployeeMapper中的测试方法
List<Employee> getAllEmployees();

// EmployeeMapper.xml映射文件中的SQL实现
<!--SQL语句字段起别名或者开启MyBatis驼峰命名配置-->
<select id="getAllEmployeesOld" resultType="Employee">
<!--select eid as eid, emp_name as empName, emp_age as empAge, emp_sex as empSex, emp_email as empEmail from t_emp-->
select * from t_emp;
</select>

<resultMap id="empResultMap" type="Employee">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="empAge" column="emp_age"/>
<result property="empSex" column="emp_sex"/>
<result property="empEmail" column="emp_email"/>
</resultMap>

<!--使用ResultMap完成列字段->对象属性映射-->
<select id="getAllEmployees" resultMap="empResultMap">
select * from t_emp;
</select>

image-20231124193722516

image-20231124193742111

2.多对一映射

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
// EmployeeMapper中的测试方法
Employee getEmpAndDept(@Param("eid") Integer eid);

// EmployeeMapper.xml映射文件中的SQL实现
<!--处理多对一映射-->
<!--1.级联属性处理多对一映射-->
<resultMap id="empAndDeptResultMap1" type="Employee">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="empAge" column="emp_age"/>
<result property="empSex" column="emp_sex"/>
<result property="empEmail" column="emp_email"/>
<result property="dept.did" column="did"/>
<result property="dept.deptName" column="dept_name"/>
</resultMap>

<!--2.association标签处理多对一映射-->
<resultMap id="empAndDeptResultMap2" type="Employee">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="empAge" column="emp_age"/>
<result property="empSex" column="emp_sex"/>
<result property="empEmail" column="emp_email"/>
<association property="dept" javaType="Department">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>

<select id="getEmpAndDept" resultMap="empAndDeptResultMap2">
select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = #{eid}
</select>

image-20231124193808309

image-20231124193832777

3.多对一映射的分步查询

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
// EmployeeMapper中的分步查询测试方法
// 优点:功能分解、可实现延迟加载
Employee getEmpAndDeptByStepOne(@Param("eid") Integer eid);
// DepartmentMapper中的分步查询测试方法
Department getEmpAndDeptByStepTwo(@Param("did") Integer did);

// EmployeeMapper.xml映射文件的SQL实现
<!--多对一映射的分步查询1-->
<!--3.association分步查询处理多对一映射-->
<resultMap id="empAndDeptResultMap3" type="Employee">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="empAge" column="emp_age"/>
<result property="empSex" column="emp_sex"/>
<result property="empEmail" column="emp_email"/>
<association property="dept"
select="com.huling.mapper.DepartmentMapper.getEmpAndDeptByStepTwo"
column="did"
fetchType="lazy"/>
</resultMap>
<select id="getEmpAndDeptByStepOne" resultMap="empAndDeptResultMap3">
select * from t_emp where eid = #{eid}
</select>

// DepartmentMapper.xml映射文件的SQL实现
<resultMap id="deptResultMap" type="Department">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
</resultMap>
<!--多对一映射的分步查询2-->
<select id="getEmpAndDeptByStepTwo" resultMap="deptResultMap">
select * from t_dept where did = #{did}
</select>

image-20231124193855902

image-20231124193921286

4.一对多映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DepartmentMapper接口中的一对多映射测试方法
Department getDeptAndEmp(@Param("did") Integer did);

// DepartmentMapper.xml映射文件的SQL实现
<resultMap id="deptAndEmpResultMap" type="Department">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
<collection property="employees" ofType="Employee">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="empAge" column="emp_age"/>
<result property="empSex" column="emp_sex"/>
<result property="empEmail" column="emp_email"/>
</collection>
</resultMap>
<!--一对多映射-->
<select id="getDeptAndEmp" resultMap="deptAndEmpResultMap">
select * from t_dept left join t_emp on t_dept.did = t_emp.did where t_dept.did = #{did}
</select>

image-20231124194146090

image-20231124194225270

5.一对多映射的分布查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// DepartmentMapper中的分步查询测试方法
Department getDeptAndEmpByStepOne(@Param("did") Integer did);
// EmployeeMapper中的分步查询测试方法
Employee getDeptAndEmpByStepTwo(@Param("did") Integer did);

// DepartmentMapper.xml映射文件中的SQL实现
<resultMap id="deptAndEmpByStepResultMap" type="Department">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
<collection property="employees"
select="com.huling.mapper.EmployeeMapper.getDeptAndEmpByStepTwo"
column="did"
fetchType="lazy"/>
</resultMap>
<!--一对多映射的分步查询1-->
<select id="getDeptAndEmpByStepOne" resultMap="deptAndEmpByStepResultMap">
select * from t_dept where did = #{did}
</select>

// EmployeeMapper.xml映射文件中的SQL实现
<select id="getDeptAndEmpByStepTwo" resultType="Employee">
select * from t_emp where did = #{did}
</select>

image-20231124194549924

image-20231124194613148

三、MyBatis的动态SQL

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。下面通过DynamicMapper接口的三个测试方法帮助理解动态SQL的应用场景:

1
2
3
4
5
6
7
8
9
10
public interface DynamicSQLMapper {
// 多条件查询
List<Employee> getEmployeeByCondition(Employee employee);

// 通过数组批量删除
int deleteMoreByArray(@Param("eids") Integer[] eids);

// 通过List批量添加
int insertMoreByList(@Param("employees") List<Employee> employees);
}

重点看一下DynamicMapper.xml映射文件如何实现的:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huling.mapper.DynamicSQLMapper">
<!--SQL片段,方便复用-->
<sql id="empColumns">eid,emp_name,emp_age,emp_sex,emp_email,did</sql>

<!--使用动态SQL最常见情景是根据条件包含where子句的一部分-->
<select id="getEmployeeByConditionOld1" resultType="Employee">
select <include refid="empColumns"/> from t_emp where 1=1
<if test="empName != null and empName != ''">
and emp_name = #{empName}
</if>
<if test="empAge != null">
and emp_age = #{empAge}
</if>
<if test="empSex != null and empSex != ''">
and emp_sex = #{empSex}
</if>
<if test="empEmail != null and empEmail != ''">
and emp_email = #{empEmail}
</if>
</select>

<!--where元素只会在子元素返回任何内容的情况下才插入“WHERE”子句。而且,若子句的开头为“AND”或“OR”,where元素也会将它们去除-->
<select id="getEmployeeByConditionOld2" resultType="Employee">
select <include refid="empColumns"/> from t_emp
<where>
<if test="empName != null and empName != ''">
emp_name = #{empName}
</if>
<if test="empAge != null">
and emp_age = #{empAge}
</if>
<if test="empSex != null and empSex != ''">
and emp_sex = #{empSex}
</if>
<if test="empEmail != null and empEmail != ''">
and emp_email = #{empEmail}
</if>
</where>
</select>

<!--自定义trim元素来定制where元素-->
<select id="getEmployeeByCondition" resultType="Employee">
select <include refid="empColumns"/> from t_emp
<trim prefix="where" prefixOverrides="and|or">
<if test="empName != null and empName != ''">
emp_name = #{empName}
</if>
<if test="empAge != null">
and emp_age = #{empAge}
</if>
<if test="empSex != null and empSex != ''">
and emp_sex = #{empSex}
</if>
<if test="empEmail != null and empEmail != ''">
and emp_email = #{empEmail}
</if>
</trim>
</select>

<!--foreach元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!-->
<delete id="deleteMoreByArray1">
delete from t_emp where eid in
<foreach collection="eids" item="eid" separator="," open="(" close=")">
#{eid}
</foreach>
</delete>

<delete id="deleteMoreByArray">
delete from t_emp where
<foreach collection="eids" item="eid" separator="or">
eid = #{eid}
</foreach>
</delete>

<!--你可以将任何可迭代对象(如List、Set等)、Map对象或者数组对象作为集合参数传递给foreach。当使用可迭代对象或者数组时,index是当前迭代的序号,item的值是本次迭代获取到的元素。当使用Map对象(或者Map.Entry对象的集合)时,index是键,item是值。-->
<insert id="insertMoreByList">
insert into t_emp values
<foreach collection="employees" item="employee" separator=",">
(null,#{employee.empName},#{employee.empAge},#{employee.empSex},#{employee.empEmail},null)
</foreach>
</insert>
</mapper>

image-20231124200604869

image-20231124200724453

四、MyBatis的缓存

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

默认情况下,只启用了本地会话缓存,它仅仅对一个会话中的数据进行缓存。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

1
<cache/>

基本上就是这样。这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

这些属性可以通过 cache 元素的属性来修改。比如:

1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。速度上会慢一些,但是更安全,因此默认值是 false。

二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。默认情况下,语句会这样来配置:

1
2
3
4
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

image-20231124222816225

image-20231124222859642