1. 前言

Mybatis 的基础知识以及相关用法因为在工作工作过程中天天都在使用,本来不打算写的。但在看 Mybatis 的源码过程中发现很多地方都对 XML 在解析,还是觉得有必要记录一下 Mybatis 的基本概念。


2. Mybatis 基本概念

Mybatis : 官网地址

Mybatis 是一款优秀的 持久层框架/半自动 的 ORM 框架,半自动的原因是因为 移植性不行,例如 OracleMySQL 就会存在大量关键字不可用。

优点

  • JDBC 相比,减少了50%的代码量。(加载驱动、获取数据库链接、设置参数和获取结果集等)
  • 上手简单,学习成本很低。
  • 实现了代码与 SQL 的解耦(提供XML标签,支持编写动态SQL

缺点

  • SQL 语句编写工作量大,熟练度要高(针对传统的金融业务,有时可能一个简单的业务也要关联七八张表)

  • 数据库移植性比较差,如果需要切换数据库的话,SQL 语句会有很大的差异

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <if test="_databaseId=='postgresql'">
    to_char(creation_time, 'yyyy-MM-dd') creation_time,
    </if>
    <if test="_databaseId=='oracle'">
    to_char(creation_time, 'yyyy-MM-dd') creation_time,
    </if>
    <if test="_databaseId=='mysql'">
    date_format(creation_time, '%Y-%m-%d') creation_time,
    </if>

3. Mybatis 快速入门

总体来说上手成本还是挺低的,比较简单。后面我们一般会集成到 Spring 中,不会像如下这样操作。

目录结构如下

image-20230818121811649

具体步骤如下

  • 导入依赖
  • 创建数据表
  • 创建实体对象
  • 创建Mapper接口
  • 编写配置文件
  • 编写测试类

3.1. 导入依赖

  • pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>

3.2. 创建数据表

  • emp.sql
1
2
3
4
5
6
7
8
9
10
-- 建表语句
CREATE TABLE `emp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 新增语句
INSERT INTO emp ( id, username )
VALUES ( 1, 'Tom' ), ( 2, 'Jack' ), ( 3, 'Tony');

3.3. 创建实体对象

  • employee.java
1
2
3
4
5
6
7
public class Employee {
private Integer id;
private String username;

// 省略 Setter/Getter

}

3.4. 创建Mapper接口

  • EmployeeMapper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface EmployeeMapper {
// 查询所有 Employee 信息
List<Employee> listByEmployee();

// 根据id查询
Employee selectEmployee(Integer id);

// 插入
Integer insertEmployee(Employee emp);

// 更新
Integer updateEmployee(Employee emp);

// 删除
Integer deleteEmployee(Integer id);
}

3.5. 编写配置文件

  • mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<configuration>
<!-- 数据库环境配置 -->
<environments default="development">
<!-- 开发环境配置 -->
<environment id="development">
<transactionManager type="JDBC"/> <!-- 使用 JDBC 事务管理 -->
<dataSource type="POOLED"> <!-- 使用连接池的数据源 -->
<property name="driver" value="com.mysql.jdbc.Driver"/> <!-- 数据库驱动类 -->
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <!-- 数据库 URL -->
<property name="username" value="root"/> <!-- 数据库用户名 -->
<property name="password" value="root"/> <!-- 数据库密码 -->
</dataSource>
</environment>
</environments>

<!-- 映射器配置 -->
<mappers>
<mapper resource="EmployeeMapper.xml"/>
</mappers>
</configuration>
  • EmployeeMapper.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
<?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.wickson.mapper.EmployeeMapper">

<!-- 根据id查询Employee -->
<select id="selectEmployee" resultType="com.wickson.entity.Employee">
select * from emp where id = #{id}
</select>

<!-- 查询所有 Employee 信息 -->
<select id="listByEmployee" resultType="com.wickson.entity.Employee">
SELECT * FROM emp
</select>

<!-- 插入 -->
<insert id="insertEmployee">
INSERT INTO emp ( `username`) VALUES (#{username});
</insert>

<!-- 更新 -->
<update id="updateEmployee">
UPDATE emp SET username = #{username} WHERE id = #{id}
</update>

<!-- 删除 -->
<delete id="deleteEmployee">
DELETE FROM emp WHERE id = #{id}
</delete>

</mapper>

3.6. 编写测试类

  • EmployeeMapperTest.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
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
public class EmployeeMapperTest {

SqlSessionFactory sqlSessionFactory;

@Before
public void init(){
// 从 XML 中构建 SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}

@After
public void listByEmployee() {
// 使用 JDK8 新特性 自动关流
try (SqlSession session = sqlSessionFactory.openSession()) {
// 使用映射器命名空间执行查询操作
List<Employee> employees = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee");
employees.forEach(System.out::println);
}
}

@Test
public void selectEmployee() {
try (SqlSession session = sqlSessionFactory.openSession()) {
// 使用映射器接口方法执行查询操作
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = mapper.selectEmployee(1);
// Employee{id=1, username='Tom'}
System.out.println(employee);
}
}

@Test
public void insertEmployee() {
try (SqlSession session = sqlSessionFactory.openSession()) {
// 使用映射器接口方法执行查询操作
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = new Employee();
employee.setUsername("Jerry");
Integer rows = mapper.insertEmployee(employee);
// 提交事务
session.commit();
// Affected rows = 1
System.out.println("Affected rows = " + rows);
}
}

@Test
public void updateEmployee() {
try (SqlSession session = sqlSessionFactory.openSession()) {
// 使用映射器接口方法执行查询操作
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = new Employee();
employee.setId(5);
employee.setUsername("Jerry1");
Integer rows = mapper.updateEmployee(employee);
// 提交事务
session.commit();
// Affected rows = 1
System.out.println("Affected rows = " + rows);
}
}

@Test
public void deleteEmployee() {
try (SqlSession session = sqlSessionFactory.openSession()) {
// 使用映射器接口方法执行查询操作
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Integer rows = mapper.deleteEmployee(5);
// 提交事务
session.commit();
// Affected rows = 1
System.out.println("Affected rows = " + rows);
}
}

}

4. Mybatis 配置文件

Mybatis 的配置文件分为两大类,第一个是 全局配置文件,第二个是 SQL 映射文件。

这部分的文件在源码分析时全部都会进行加载

4.1. 全局配置文件

如下是 Mybatis 的全局配置文件信息,后面的源码信息会加载如下配置信息

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<!-- 导入数据库属性配置文件 -->
<properties resource="db.properties"></properties>

<!-- MyBatis全局设置 -->
<settings>
<!-- 配置下划线自动映射为驼峰命名法 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

<!-- 类型别名 -->
<typeAliases>
<!-- 扫描指定包下的类,作为类型别名 -->
<package name="com.wickson.bean"/>
</typeAliases>

<!-- 数据库环境配置 -->
<environments default="development">
<!-- 开发环境 -->
<environment id="development">
<!-- 使用JDBC事务管理 -->
<transactionManager type="JDBC"/>
<!-- 使用连接池数据源 -->
<dataSource type="POOLED">
<!-- 数据库连接信息从外部属性文件获取 -->
<property name="driver" value="${driverClassname}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

<!-- 数据库厂商标识配置 -->
<databaseIdProvider type="DB_VENDOR">
<!-- 针对不同数据库的标识 -->
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="orcl"/>
</databaseIdProvider>

<!-- 映射器配置 -->
<mappers>
<!-- 单个映射器文件的引入 -->
<mapper resource="EmployeeMapper.xml"/>
<!-- 扫描指定包下的映射器接口 -->
<package name="com.wickson.mapper"/>
</mappers>
</configuration>

4.2. SQL 映射文件

SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。
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
<?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="cn.tulingxueyuan.dao.EmployeeMapper">
<!-- MyBatis SQL映射文件 -->

<!-- 缓存设置 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

<!-- 引用外部缓存 -->
<cache-ref namespace="com.wickson.mapper.DepartmentMapper"/>

<!-- 结果映射 -->
<resultMap id="employeeResultMap" type="com.wickson.entity.Employee">
<!-- 配置映射关系,这里以id为例 -->
<id property="id" column="emp_id"/>
<result property="empName" column="emp_name"/>
<result property="email" column="emp_email"/>
</resultMap>

<!-- SQL片段定义 -->
<sql id="baseColumnList">
emp_id, emp_last_name, emp_email
</sql>

<!-- 插入操作
useGeneratedKeys: 仅适用于 insert 和 update, 取出由数据库内部生成的主键,默认值:false。
keyProperty: 指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值
-->
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="id">
<!-- 插入SQL语句 -->
INSERT INTO employees(${baseColumnList})
VALUES(#{id}, #{lastName}, #{email})
</insert>

<!-- 更新操作 -->
<update id="updateEmployee" parameterType="com.wickson.entity.Employee">
<!-- 更新SQL语句 -->
UPDATE employees
SET emp_last_name = #{lastName}, emp_email = #{email}
WHERE emp_id = #{id}
</update>

<!-- 删除操作 -->
<delete id="deleteEmployee" parameterType="int">
<!-- 删除SQL语句 -->
DELETE FROM employees WHERE emp_id = #{id}
</delete>

<!-- 查询操作 -->
<select id="getEmployeeById" resultMap="employeeResultMap" parameterType="int">
<!-- 查询SQL语句 -->
SELECT ${baseColumnList}
FROM employees
WHERE emp_id = #{id}
</select>
</mapper>

5. Mybatis 动态 SQL

5.1. if

根据条件生成 SQL 片段

1
2
3
4
5
6
<select id="findActiveBlogWithTitleLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>

5.2. choose / when / otherwise

类似于Java中的switch语句,根据条件选择不同的分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>

5.3. foreach

用于循环遍历集合,生成对应的 SQL 片段

  1. 如果传入的是单参数且参数类型是一个 List 的时候,collection 属性值为 list
  2. 如果传入的是单参数且参数类型是一个array数组的时候,collection 的属性值为 array
  3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在 MyBatis 里面也是会把它封装成一个 Map 的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="selectPostIn" resultType="Blog">
SELECT * FROM BLOG
<where>
<!--
item   表示集合中每一个元素进行迭代时的别名,随便起的变量名;
index   指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;
open   表示该语句以什么开始,常用“(”;
separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”;
close   表示以什么结束,常用“)”。
-->
<foreach item="item" index="index" collection="list" open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>

5.4. set

可以用在动态更新的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">
username=#{username},
</if>
<if test="password != null">
password=#{password},
</if>
<if test="email != null">
email=#{email},
</if>
<if test="bio != null">
bio=#{bio}
</if>
</set>
where id=#{id}
</update>

5.5. where

可以用在所有的查询条件都是动态的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>

5.6. bind

用于将表达式绑定为一个变量,以便在后续的 SQL 语句中使用。

1
2
3
4
<if test="param.filter != null and param.filter!=''">
<bind name="filterLike" value="'%' + param.filter + '%'"/>
and ( name like #{filterLike,jdbcType=VARCHAR} )
</if>

6. Mybatis 缓存

Mybatis 缓存一般分为两种:

一级缓存:线程级别的缓存,是本地缓存,sqlSession 级别的缓存

二级缓存:全局范围的缓存,不止局限于当前会话

MyBatis 的二级缓存在某些场景下可以提高系统性能,但在大多数情况下不推荐使用,可能导致数据不一致性、内存占用过高、缓存同步问题和对复杂查询结果的管理困难

6.1. 一级缓存

基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 SqlSession,各个 SqlSession 之间的缓存相互隔离,当 Session flush 或 close 之后,该 SqlSession 中的所有 Cache 就将清空,MyBatis 默认打开一级缓存。

  • 图解

image-20230823215447415

  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void cache(){
try (SqlSession session = sqlSessionFactory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
List<Employee> employees = mapper.listByEmployee();
employees.forEach(System.out::println);
System.out.println(" ------------ 默认开启一级缓存 ------------ ");
// 使用映射器命名空间执行查询操作
List<Employee> employeeList = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee");
employeeList.forEach(System.out::println);
}
}
  • 结果(连续调用两次,但只执行了一次 SQL
1
2
3
4
5
6
7
8
9
10
22:06:38.651 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==>  Preparing: SELECT * FROM emp 
22:06:38.668 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==> Parameters:
22:06:38.677 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - <== Total: 3
Employee{id=1, username='Tom'}
Employee{id=2, username='Jack'}
Employee{id=3, username='Tony'}
------------ 默认开启一级缓存 ------------
Employee{id=1, username='Tom'}
Employee{id=2, username='Jack'}
Employee{id=3, username='Tony'}

失效情况

  1. 不同的 SqlSession 会使一级缓存失效。
  2. 同一个 SqlSession,但是查询语句不一样。
  3. 同一个 SqlSession,查询语句一样,期间执行增删改操作。
  4. 同一个 SqlSession,查询语句一样,执行手动清除缓存。

6.2. 二级缓存

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCacheHashMap 存储,不同之处在于其存储作用域为 Mapper(Namespace),可以在多个SqlSession 之间共享,并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置。

  • 图解(缓存查询的顺序是先查询二级缓存再查询一级缓存)

Mybatis二级缓存示意图

  • 使用

    • 在全局配置(mybatis-config.xml)文件开启 二级缓存
    1
    2
    3
    4
    5
    <!-- 开启全局配置文件-->
    <settings>
    <!-- Mybatis 二级缓存 -->
    <setting name="cacheEnabled" value="true"/>
    </settings>
    • 需要在使用二级缓存的映射文件出使用标签标注
    1
    <cache/>
    • 实体类必须要实现 Serializable 接口
    1
    2
    3
    4
    5
    6
    7
    8
    public class Employee implements Serializable  {

    private Integer id;

    private String username;

    // 省略 setter/getter
    }
    • 测试
    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
    public class EmployeeMapperTest {

    SqlSessionFactory sqlSessionFactory;

    @Before
    public void init(){
    // 从 XML 中构建 SqlSessionFactory
    String resource = "mybatis-config.xml";
    InputStream inputStream = null;
    try {
    inputStream = Resources.getResourceAsStream(resource);
    } catch (IOException e) {
    e.printStackTrace();
    }
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void cache(){
    try (SqlSession session = sqlSessionFactory.openSession()) {
    EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
    List<Employee> employees = mapper.listByEmployee();
    employees.forEach(System.out::println);
    System.out.println(" ------------ 开启二级缓存 ------------ ");
    // 使用映射器命名空间执行查询操作
    List<Employee> employeeList = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee");
    employeeList.forEach(System.out::println);
    }
    }

    @After
    public void listByEmployee() {
    // 使用 JDK8 新特性 自动关流
    try (SqlSession session = sqlSessionFactory.openSession()) {
    // 使用映射器命名空间执行查询操作
    List<Employee> employees = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee");
    employees.forEach(System.out::println);
    }
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    22:29:12.611 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==>  Preparing: SELECT * FROM emp 
    22:29:12.626 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==> Parameters:
    22:29:12.635 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - <== Total: 3
    Employee{id=1, username='Tom'}
    Employee{id=2, username='Jack'}
    Employee{id=3, username='Tony'}
    ------------ 开启二级缓存 ------------
    22:29:12.635 [main] DEBUG com.wickson.mapper.EmployeeMapper - Cache Hit Ratio [com.wickson.mapper.EmployeeMapper]: 0.0
    Employee{id=1, username='Tom'}
    Employee{id=2, username='Jack'}
    Employee{id=3, username='Tony'}
    22:29:12.642 [main] DEBUG com.wickson.mapper.EmployeeMapper - Cache Hit Ratio [com.wickson.mapper.EmployeeMapper]: 0.3333333333333333
    Employee{id=1, username='Tom'}
    Employee{id=2, username='Jack'}
    Employee{id=3, username='Tony'}

失效

  1. 同一个命名空间进行了增删改的操作,会导致二级缓存失效,但是如果不想失效:可以将 SQLflushCache 这是为 false ,但是要慎重设置,因为会造成数据脏读问题,除非你能保证查询的数据永远不会执行增删改

  2. 让查询不缓存数据到二级缓存中 useCache="false"

  3. 如果希望其他 mapper 映射文件的命名空间执行了增删改清空另外的命名空间就可以设置:

    1
    <cache-ref namespace="com.wickson.mapper.DeptMapper"/>