MyBatis工作原理
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
MyBatis 有三个基本要素:
- 核心接口和类
- MyBatis核心配置文件(mybatis-config.xml)
- SQL映射文件(mapper.xml)

每个MyBatis应用程序都以一个SqlSessionFactory对象的实例为核心。
-
首先,获取SqlSessionFactoryBuilder对象,可以根据XML配置文件或者Configuration类的实例构建该对象。
-
然后,获取SqlSessionFactory对象,该对象实例可以通过SqlSessionFactoryBuilder对象来获取。
-
最后,有了SqlSessionFactory对象之后,就可以进而获取SqlSession实例。SqlSession对象中完全包含以数据库为背景的所有执行SQL操作的方法,用该实例可以直接执行已映射的SQL语句。
MyBatis的整体工作流程如下图所示:

SqlSessionFactoryBuilder
SqlSessionFactoryBuilder会根据配置信息或者代码生成SqlSessionFactory,并且提供了多个build()方法重载。

SqlSessionFactory
SqlSessionFactory是工厂接口而不是现实类,他的任务就是创建SqlSession。
所有的MyBatis应用都以SqlSessionFactory实例为中心,SqlSessionFactory的实例可以通过SqlSessionFactoryBuilder对象来获取。有了它以后,顾名思义,就可以通过 SqlSession提供的openSession()方法来获取SqlSession实例。源码如下。

SqlSessionFactory的生命周期和作用域: SqlSessionFactory对象一旦创建,就会在整个应用程序过程中始终存在。没有理由去销毁或再创建它,并且在应用程序运行中也不建议多次创建SqlSessionFactory。因此SqlSessionFactory的最佳作用域是Application,即随着应用程序的生命周期一直存在。这种“存在于整个应用运行期间,并且只存在一个对象实例”的模式就是所谓的单例模式(指在运行期间有且仅有一个实例)。
SqlSession
SqlSession 是用于执行持久化操作的对象,类似于JDBC中的Connection。它提供了面向数据库执行SQL命令所需的所有方法,可以通过SqlSession实例直接运行已映射的 SQL语句。

SqlSession的用途主要有两种。
SqlSession生命周期和作用域: SqlSession对应一次数据库会话。由于数据库会话不是永久的,因此SqlSession的生命周期也不是永久的,每次访问数据库时都需要创建 SqlSession对象。需要注意的是:每个线程都有自己的SqlSession实例,SqlSession实例不能被共享,也不是线程安全的。因此SqlSession的作用域范围是request作用域或方法体作用域内。
MyBatis实现逻辑
下面是一个最基本的MyBatis的例子,MyBatis不是一定要跟Spring,SpringBoot在一起才能工作的,它并不依赖于Spring体系,下面是一个基础的MyBatis独立使用的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class MyBatisTest {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
Person person = mapper.selectPersonById(1);
System.out.println("AGE: " + person.getAge());
}
}
|
1
2
3
4
5
6
7
|
public class Person {
private String name;
private int gender;
private int age;
// 省略getter&setter
}
|
1
2
3
4
|
@Mapper
public interface PersonMapper {
Person selectPersonById(@Param("id") Integer id);
}
|
1
2
3
4
5
6
7
8
9
|
<?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="net.liwenbo.mybatis.PersonMapper">
<select id="selectPersonById" resultType="net.liwenbo.mybatis.Person">
select * from person where id = #{id}
</select>
</mapper>
|
mybatis.xml,这里使用的是Sql Server,可以很容易换成mysql或其他数据库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<property name="url" value="jdbc:sqlserver://localhost:1433;databaseName=socc"/>
<property name="username" value="sa"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/PersonMapper.xml"/>
</mappers>
</configuration>
|
pom.xml,只依赖了mybatis和sqlserver的驱动程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.liwenbo.myabtis</groupId>
<artifactId>mybatis-test</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>9.2.1.jre8</version>
</dependency>
</dependencies>
</project>
|
核心原理
MyBatis的实现原理的核心是动态代理,考虑上面的例子,PersonMapper是一个接口,我们定义了方法selectPersonById,但我们的代码并没有任何实现这个方法的代码。那么程序运行的时候,是怎么确定这个接口要执行什么样的逻辑的呢?
通过调试上面的代码,可以看到,当mapper执行selectPersonById的时候,mapper变量实际上是一个动态代理的对象。

而执行语句的时候,是通过MapperProxy类中的invoke方法执行的。
1
2
3
4
5
6
7
8
9
10
11
12
|
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
|
而与数据库交互的sql语句的执行,其核心的方法是MapperMethod.java中的execute方法。
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
|
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
|
MyBatis映射器
映射器是 MyBatis 中最重要的文件,文件中包含一组 SQL 语句(例如查询、添加、删除、修改),这些语句称为映射语句或映射 SQL 语句。
映射器由 Java 接口和 XML 文件(或注解)共同组成,它的作用如下。
- 定义参数类型
- 配置缓存
- 提供 SQL 语句和动态 SQL
- 定义查询结果和 POJO 的映射关系
映射器有以下两种实现方式。
- 通过 XML 文件方式实现,比如我们在 mybatis-config.xml 文件中描述的 XML 文件,用来生成 mapper。
- 通过注解的方式实现,使用 Configuration 对象注册 Mapper 接口。
如果 SQL 语句存在动态 SQL 或者比较复杂,使用注解写在 Java 文件里可读性差,且增加了维护的成本。所以一般建议使用 XML 文件配置的方式,避免重复编写 SQL 语句。
MyBatis 映射器的主要元素

resultMap
resultMap 是 MyBatis 中最复杂的元素,主要用于解决实体类属性名与数据库表中字段名不一致的情况,可以将查询结果映射成实体对象。下面我们先从最简单的功能开始介绍。
resultMap 元素还可以包含以下子元素,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<resultMap id="" type="">
<constructor> <!-- 类再实例化时用来注入结果到构造方法 -->
<idArg/> <!-- ID参数,结果为ID -->
<arg/> <!-- 注入到构造方法的一个普通结果 -->
</constructor>
<id/> <!-- 用于表示哪个列是主键 -->
<result/> <!-- 注入到字段或JavaBean属性的普通结果 -->
<association property=""/> <!-- 用于一对一关联 -->
<collection property=""/> <!-- 用于一对多、多对多关联 -->
<discriminator javaType=""> <!-- 使用结果值来决定使用哪个结果映射 -->
<case value=""/> <!-- 基于某些值的结果映射 -->
</discriminator>
</resultMap>
|
其中:
- <resultMap> 元素的 type 属性表示需要的 POJO,id 属性是 resultMap 的唯一标识。
- 子元素 <constructor> 用于配置构造方法。当一个 POJO 没有无参数构造方法时使用。
- 子元素 <id> 用于表示哪个列是主键。允许多个主键,多个主键称为联合主键。
- 子元素 <result> 用于表示 POJO 和 SQL 列名的映射关系。
- 子元素 <association>、<collection> 和 <discriminator> 用在级联的情况下。关于级联的问题比较复杂
id 和 result 元素都有以下属性:

结果映射
一条 SQL 查询语句执行后会返回结果集,结果集有两种存储方式,即使用 Map 存储 和 使用 POJO 存储。
Map存储结果集
任何 select 语句都可以使用 Map 存储,例如:
1
2
3
4
|
<!-- 查询所有网站信息存到Map中 -->
<select id="selectAllPersons" resultType="map">
select * from person
</select>
|
在 PersonMapper 接口中添加以下方法:
1
|
public List<Map<String, Object>> selectAllPersons();
|
Map 的 key 是 select 语句查询的字段名(必须完全一样),而 Map 的 value 是查询返回结果中字段对应的值,一条记录映射到一个 Map 对象中。使用 Map 存储结果集很方便,但可读性稍差,所以一般推荐使用 POJO 的方式。
POJO存储结果集
因为 MyBatis 提供了自动映射,所以使用 POJO 存储结果集是最常用的方式。但有时候需要更加复杂的映射或级联,这时就需要使用 select 元素的 resultMap 属性配置映射集合。
Person.java
1
2
3
4
5
6
7
|
public class Person {
private String name;
private int gender;
private int age;
// 省略Getter&Setter
}
|
PersonMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?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="net.liwenbo.mybatis.PersonMapper">
<resultMap id="personResult" type="net.liwenbo.mybatis.Person">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="age" column="age"/>
</resultMap>
<select id="selectAllPersons" resultMap="personResult">
SELECT name,gender,age FROM person
</select>
</mapper>
|
PersonMapper.java
1
2
3
4
|
@Mapper
public interface PersonMapper {
List<Person> selectAllPersons();
}
|
测试的代码及结果
1
2
3
4
5
6
7
8
9
10
|
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
List<Person> people = mapper.selectAllPersons();
people.forEach(System.out::println);
// 输出结果
liwenbo(男,36岁)
hanzhu(女,35岁)
lishiran(女,7岁)
liyiran(男,5岁)
|
resultType和resultMap的区别
MyBatis 的每一个查询映射的返回类型都是 resultMap,只是当我们提供的返回类型是 resultType 时,MyBatis 会自动把对应的值赋给 resultType 所指定对象的属性,而当我们提供的返回类型是 resultMap 时,MyBatis 会将数据库中的列数据复制到对象的相应属性上,可用于复制查询。
例如,刚才的mapper文件中,由于id,name,gender,age四个字段与Person类能完全对上,可以使用resultType替换resultMap,另一种写法为:
1
2
3
|
<select id="selectAllPersons" resultType="net.liwenbo.mybatis.Person">
SELECT id,name,gender,age FROM person
</select>
|
如果这里的字段与Person类不能完全自动匹配上,则会报某个列名无效的异常。需要注意的是,resultMap 和 resultType 不能同时使用。
MyBatis关联查询
一对一
一对一级联关系在现实生活中是十分常见的,例如一个大学生只有一个学号,一个学号只属于一个学生。同样,人与身份证也是一对一的级联关系。
在 MyBatis 中,通过 <resultMap> 元素的子元素 <association> 处理一对一级联关系。示例代码如下。
1
2
3
|
<association property="studentCard" column="cardId"
javaType="net.biancheng.po.StudentCard"
select="net.biancheng.mapper.StudentCardMapper.selectStuCardById" />
|
在 <association> 元素中通常使用以下属性。
- property:指定映射到实体类的对象属性。
- column:指定表中对应的字段(即查询返回的列名)。
- javaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。
但在实际工作中,一对一关联通常采用关联查询(左连接、右连接等)或者分布查询的方式。
- 关联查询:使用Left join,right join等联接语句,直接查询出最终结果。
- 分布查询:通过两次或多次查询,为一对一关系的实体 Bean 赋值。分布查询通常还会与缓存进行结合,对于一些不常变动的数据,可以通过缓存的方式进行分布查询。
一对多
在 MyBatis 中,通过 <resultMap> 元素的子元素 <collection> 处理一对多级联关系,collection 可以将关联查询的多条记录映射到一个 list 集合属性中。示例代码如下。
1
2
3
|
<collection property="orderList"
ofType="net.biancheng.po.Order" column="id"
select="net.biancheng.mapper.OrderMapper.selectOrderById" />
|
在 <collection> 元素中通常使用以下属性。
- property:指定映射到实体类的对象属性。
- column:指定表中对应的字段(即查询返回的列名)。
- javaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。
与一对一一样,一对多关联查询可采用以下两种方式:
- 关联查询,通过关联查询实现。
- 分步查询,通过两次或多次查询,为一对多关系的实体 Bean 赋值。
多对对
实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。MyBatis 没有实现多对多级联,推荐通过两个一对多级联替换多对多级联,以降低关系的复杂度,简化程序。
MyBatis动态SQL
动态SQL是MyBatis的强大特性之一。在JDBC或其它类似的框架中,开发人员通常需要手动拼接SQL语句。根据不同的条件拼接SQL语句是一件极其痛苦的工作。例如,拼接时要确保添加了必要的空格,还要注意去掉列表最后一个列名的逗号。而动态SQL恰好解决了这一问题,可以根据场景动态的构建查询。
动态SQL只有几个基本元素,与 JSTL 或 XML 文本处理器相似,十分简单明了,大量的判断都可以在 MyBatis 的映射 XML 文件里配置,以达到许多需要大量代码才能实现的功能。
动态SQL大大减少了编写代码的工作量,更体现了 MyBatis 的灵活性、高度可配置性和可维护性。
MyBatis的动态SQL包括以下几种元素,如下表所示。

具体的使用方法可参见MyBatis的官方文档:
https://mybatis.org/mybatis-3/zh/dynamic-sql.html
MyBatis缓存
缓存可以将数据保存在内存中,是互联网系统常常用到的。目前流行的缓存服务器有 MongoDB、Redis、Ehcache等。缓存是在计算机内存上保存的数据,读取时无需再从磁盘读入,因此具备快速读取和使用的特点。
和大多数持久化框架一样,MyBatis提供了一级缓存和二级缓存的支持。默认情况下,MyBatis只开启一级缓存。
一级缓存
一级缓存是基于PerpetualCache(MyBatis自带)的HashMap本地缓存,作用范围为 session域内。当session flush(刷新)或者close(关闭)之后,该session 中所有的cache(缓存)就会被清空。
在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个mapper 的方法,往往只执行一次SQL。因为使用SqlSession第一次查询后,MyBatis会将其放在缓存中,再次查询时,如果没有刷新,并且缓存没有超时的情况下,SqlSession会取出当前缓存的数据,而不会再次发送SQL到数据库。
由于SqlSession是相互隔离的,所以如果你使用不同的SqlSession对象,即使调用相同的Mapper、参数和方法,MyBatis还是会再次发送SQL到数据库执行,返回结果。
二级缓存
二级缓存是全局缓存,作用域超出session范围之外,可以被所有SqlSession共享。一级缓存缓存的是SQL语句,二级缓存缓存的是结果对象。
二级缓存的配置
1)MyBatis的全局缓存配置需要在mybatis-config.xml的settings元素中设置,代码如下。
1
2
3
|
<settings>
<setting name="cacheEnabled" value="true" />
</settings>
|
2)在mapper 文件(如WebMapper.xml)中设置缓存,默认不开启缓存。需要注意的是,二级缓存的作用域是针对mapper的namescape而言,即只有再次在namescape内(net.liwenbo.WebsiteMapper)的查询才能共享这个缓存,代码如下。
1
2
3
4
5
6
7
8
9
|
<mapper namescape="net.liwenbo.WebsiteMapper">
<!-- cache配置 -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true" />
...
</mapper>
|
以上属性说明如下。

对于MyBatis缓存仅作了解即可,因为面对一定规模的数据量,内置的Cache方式就派不上用场了,并且对查询结果集做缓存并不是MyBatis所擅长的,它专心做的应该是 SQL映射。对于缓存,采用Redis、Memcached等专门的缓存服务器来做更为合理。
参考