初探 MyBatis

SqlSessionFactory 的生成

MyBatis 是一个基于 SqlSessionFactory 构建的框架,对于 SqlSessionFactory 而言,它的作用是生成 SqlSession 接口,但是它们对于业务而言是单一的,在 Spring Boot 中,对于它们的实现是非显示的,去掉框架,我们可以这么做:

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
import org.apache.ibatis.io.Resources;
...
// 声明 SSF
private static SqlSessionFactory sqlSessionFactory;
...
// 创建 SSF
try {
// 读取 mybatis 配置
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
// 创建我们需要的 SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
...
// 创建 SS
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
List<Country> countryList = sqlSession.selectList("selectAll");
printCountryList(countryList);
} finally {
sqlSession.close();
}
...

Mapper 接口动态代理

在使用 MyBatis 时,我们仅仅定义接口 mapper 和与之对应的 xml ,但是并不需要做具体的 imp 类与方法,这是因为 MyBatis 在内部存在一个代理者,实现了 InvocationHandler 接口,当我们使用某个 pojo 类的方法的时候,它将获得这个 pojo 类的全限定名称(同时,也是 xml 中的 namespace ,两者相互关联),与调用的方法进行组合,这样一个方法就被唯一指定了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
public class MyMapperProxy<T> implements InvocationHandler {

private Class<T> mapperInterface;
private SqlSession sqlSession;

public MyMapperProxy(Class<T> mapperInterface, SqlSession sqlSession) {
this.mapperInterface = mapperInterface;
this.sqlSession = sqlSession;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// method.getName() 指向了将被调用的方法名,同时也是 xml 中的 namespace
// mapperInterface.getCanonicalName 会找到具体的方法
List<T> list = sqlSession.selectList(
mapperInterface.getCanonicalName() + "." + method.getName());
return list;
}
}
...

MyBatis 接口 sql 的实现

有三种方式可以实现接口中的 sql ,分别是 xml ,接口上注解,以及 provider 注解。

三种方式大同小异,为了代码一致性,推荐统一使用 xml 方式。

xml:

1
2
3
<select id="selectById" resultMap="userMap">
select * from sys_user where id = #{id}
</select>

注解:

1
2
3
4
5
6
7
8
@Select({
"select id, role_name roleName, enabled," +
"create_by createBy," +
"create_time createTime " +
"from sys_role " +
"where id = #{id}"
})
SysRole selectById(Long id);

provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// mapper 接口
...
@SelectProvider(type = PrivilegeProvider.class, method = "selectById")
SysPrivilege selectById(Long id);
...

// provider 类
...
public String selectById(final Long id) {
return new SQL() {
{
SELECT("id, privilege_name, privilege_url");
FROM("sys_privilege");
WHERE("id = #{id}");
}
}.toString();
}
...

MyBatis generator 的使用

通过 org.mybatis.generator 包可以在 xml 中进行设置来生成 model 对应的 sql 实现和 dao 接口,之后还要研究另一种实现方式,所以这里暂且不考虑内部实现,之后,我们可以使用对应 example 类来实现 sql ,这里也先不讨论。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
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
<property name="beginningDelimiter" value="'"/>
<property name="endingDelimiter" value="'"/>
<property name="javaFileEncoding" value="UTF-8"/>

<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="addRemarkComments" value="true"/>
</commentGenerator>

<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8"
userId="root"
password="czp">
</jdbcConnection>

<!--实体类生成配置-->
<javaModelGenerator targetPackage="com.koon.generator.model" targetProject="/Users/ronnie/work/GithubWorkspace/springboot-tourist/mybatis-started-to-master/chapter2/src/main/java">
<property name="trimStrings" value="true"/>
</javaModelGenerator>

<!--Mapper.xml生成配置-->
<sqlMapGenerator targetPackage="generator.mapper" targetProject="/Users/ronnie/work/GithubWorkspace/springboot-tourist/mybatis-started-to-master/chapter2/src/main/resources"/>

<!--Mapper.java生成配置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.koon.generator.mapper" targetProject="/Users/ronnie/work/GithubWorkspace/springboot-tourist/mybatis-started-to-master/chapter2/src/main/java"/>

<table tableName="country">
<generatedKey column="id" sqlStatement="MySQL" identity="true"/>
</table>
</context>
</generatorConfiguration>

一对多中 resultMap 指定

无论一对一、一对多中,我们都有可能要使用 resultMap ,其中可能是要在一个 xml 中使用另一个 xml 中的 resultMap ,那么,它的写法是先指向 mapper 接口文件,然后指向该接口文件对应 xml 中的 resultMap 的 id ,这也是 mapper 动态代理的一种应用:

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="userRoleListMap" type="com.koon.model.SysUser">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="user_password" property="userPassword"/>
<result column="user_email" property="userEmail"/>
<result column="user_info" property="userInfo"/>
<result column="head_img" property="headImg" jdbcType="BLOB"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<!--注意这里 resultMap 的指定方式-->
<collection property="roleList" columnPrefix="role_" ofType="com.koon.model.SysRole" resultMap="com.koon.mapper.RoleMapper.rolePrivilegeListMap"/>
</resultMap>

MyBatis 缓存

MyBatis 中存在一级缓存和二级缓存,一级缓存是指在一个 SqlSession 生命周期下相同键值存入一个 Map 对象中,可以通过参数修改来破除一级缓存:

1
2
3
4
<!--通过 id 查询用户-->
<select id="selectById" resultMap="userMap" flushCache="true">
select * from sys_user where id = #{id}
</select>

二级缓存不止存在于 SqlSession 生命周期中,它存在于 SqlSessionFactory 生命周期中,在 mybatis-config.xml 中,可以对它进行设置,默认就是 true :

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

MyBaits 使用 SeiralizedCache 序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存换取数据的时候,得到的是一个新的实例,所以我们常常对可序列化对象这样操作:

1
2
3
4
public class SysUser implements Serializable {
private static final long serialVersionUID = -8280230645677496806L;
...
}

在 xml 文件和对应 mapper 接口下,我们都可以进行 cache 配置,二级缓存和命名空间绑定,所以通常每一个 mapper 都拥有自己的二级缓存。

1
2
3
4
5
6
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
1
2
3
4
5
6
7
<!--二级缓存-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="false"
/>

通过参照缓存,来确保我们在同一个二级缓存中,将同一个事务的对象设置为一个二级缓存,可以有效避免脏数据。

1
@CacheNamespaceRef(RoleMapper.class)
1
<cache-ref namespace="com.koon.mapper.RoleMapper"/>

后续将跟进通用 mapper 和分页插件