MyBatis是Apache的一个开源项目iBatis,是一种基于Java的持久层框架。相信很多的从事java开发的朋友都使用过该框架。鄙人也是使用MyBatis中的一员,但是也是只限于使用而已,现在想深入了解一下MyBatis的底层架构以及其他方面的知识,提升自己的开发水平和架构能力。好记性不如烂笔头,做一下笔记方便自己在以后的工作中可以温故而知新。
1.MyBatis一级缓存
MyBatis的一级缓存在MyBatis中是默认打开的,是SqlSession级别的缓存,即MyBatis在操作数据库时应先创建SqlSession对象,在对象中有一个HashMap类型的数据结构来存储缓存数据,在不同的SqlSession对象中的缓存数据是不可以共享的,即缓存数据在不同的SqlSession中保持独立。
1.1 MyBatis一级缓存实现UML图
1.2 MyBatis一级缓存查询命中原则
mapper.xml示例代码如下
<?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="org.yoko.com.dao.UserInfoDao">
<resultMap type="org.yoko.com.entity.UserInfo" id="UserInfoMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="VARCHAR"/>
</resultMap>
<select id="queryById1" resultMap="UserInfoMap">
select id, name, age from user_info where id = #{id}
</select>
<select id="queryById2" resultMap="UserInfoMap">
select id, name, age from user_info where id = #{id}
</select>
<select id="queryById3" parameterType="java.util.Map" resultMap="UserInfoMap">
select id, name, age from user_info where id = #{id}
</select>
<select id="list" resultMap="UserInfoMap">
select id, name, age from user_info where 1=1
</select>
<select id="queryById4" parameterType="java.util.Map" resultMap="UserInfoMap">
<if test="type == 1">
select id, name, age from user_info where id = #{id}
</if>
<if test="type == 2">
select id, name, age from user_info where 1=1 and id = #{id}
</if>
</select>
</mapper>
获取SqlSession示例代码如下
private SqlSession getSqlSession() throws IOException {
// 加载配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 开启SqlSession
return sqlSessionFactory.openSession();
}
1.2.1 statementId命中,即xxxMapper.xml中的select标签中的id名称
在mapper.xml示例代码中可以发现queryById1和queryById2两个查询,只有Id不同,其他全部相同。测试示例代码如下:
@Test
public void statementIdTest() throws IOException {
SqlSession sqlSession = this.getSqlSession();
UserInfo userInfo1 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById1", 1);
logger.info("第一次查询结果: " + userInfo1);
UserInfo userInfo2 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById2", 1);
logger.info("第二次查询结果: " + userInfo2);
logger.info("第一次查询结果与第二次查询结果是否相同: " + (userInfo1 == userInfo2));
UserInfo userInfo3 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById1", 1);
logger.info("第三次查询结果:" + userInfo3);
logger.info("第一次查询结果与第三次查询结果是否相同: " + (userInfo3 == userInfo1));
}
控制台日志输出结果:
2020-09-25 14:38:52,668 DEBUG [org.yoko.com.dao.UserInfoDao] - Cache Hit Ratio [org.yoko.com.dao.UserInfoDao]: 0.0
2020-09-25 14:38:53,343 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 14:38:53,371 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Parameters: 1(Integer)
2020-09-25 14:38:53,427 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - <== Total: 1
2020-09-25 14:38:53,427 INFO [LocalCacheTest] - 第一次查询结果: UserInfo(id=1, name=赵一, age=21)
2020-09-25 14:38:53,427 DEBUG [org.yoko.com.dao.UserInfoDao] - Cache Hit Ratio [org.yoko.com.dao.UserInfoDao]: 0.0
2020-09-25 14:38:53,427 DEBUG [org.yoko.com.dao.UserInfoDao.queryById2] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 14:38:53,428 DEBUG [org.yoko.com.dao.UserInfoDao.queryById2] - ==> Parameters: 1(Integer)
2020-09-25 14:38:53,429 DEBUG [org.yoko.com.dao.UserInfoDao.queryById2] - <== Total: 1
2020-09-25 14:38:53,429 INFO [LocalCacheTest] - 第二次查询结果: UserInfo(id=1, name=赵一, age=21)
2020-09-25 14:38:53,429 INFO [LocalCacheTest] - 第一次查询结果与第二次查询结果是否相同: false
2020-09-25 14:38:53,429 DEBUG [org.yoko.com.dao.UserInfoDao] - Cache Hit Ratio [org.yoko.com.dao.UserInfoDao]: 0.0
2020-09-25 14:38:53,429 INFO [LocalCacheTest] - 第三次查询结果:UserInfo(id=1, name=赵一, age=21)
2020-09-25 14:38:53,429 INFO [LocalCacheTest] - 第一次查询结果与第三次查询结果是否相同: true
通过控制台日志输出可以看到,第一次查询和第二次查询会查询数据库,虽然其查询SQL语句相同,返回数据相同,但是通过“==”判定是false;而第三次查询没有打印SQL语句,说明其没有查询数据库,而是读取的第一次查询的缓存结果,所以其“==”判定为true.
1.2.2 查询参数一致,指实际SQL中传递的参数值
测试示例代码如下:
@Test
public void paramTest() throws IOException {
SqlSession sqlSession = this.getSqlSession();
// 第一种,参数类型为基本数据类型
UserInfo userInfo1 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById1", 1);
logger.info("第一次查询结果:" + userInfo1);
UserInfo userInfo2 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById1", 1);
logger.info("第二次查询结果:" + userInfo2);
logger.info("第一次查询结果与第二次查询是否相同:" + (userInfo1 == userInfo2));
// 第二种,参数类型为对象类型
Map<String, Object> param1 = new HashMap<String, Object>();
param1.put("id",1);
param1.put("name","第一次");
UserInfo userInfo3 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById3", param1);
logger.info("第三次查询结果:" + userInfo3);
Map<String, Object> param2 = new HashMap<String, Object>();
param2.put("id",1);
param2.put("name","第二种");
UserInfo userInfo4= sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById3", param2);
logger.info("第四次查询结果:" + userInfo4);
logger.info("第三次查询结果与第四次查询是否相同:" + (userInfo3 == userInfo4));
}
控制台日志输出结果:
2020-09-25 15:08:13,946 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 15:08:13,984 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Parameters: 1(Integer)
2020-09-25 15:08:14,027 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - <== Total: 1
2020-09-25 15:08:14,028 INFO [LocalCacheTest] - 第一次查询结果:UserInfo(id=1, name=赵一, age=21)
2020-09-25 15:08:14,028 INFO [LocalCacheTest] - 第二次查询结果:UserInfo(id=1, name=赵一, age=21)
2020-09-25 15:08:14,028 INFO [LocalCacheTest] - 第一次查询结果与第二次查询是否相同:true
2020-09-25 15:08:14,028 DEBUG [org.yoko.com.dao.UserInfoDao.queryById3] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 15:08:14,028 DEBUG [org.yoko.com.dao.UserInfoDao.queryById3] - ==> Parameters: 1(Integer)
2020-09-25 15:08:14,030 DEBUG [org.yoko.com.dao.UserInfoDao.queryById3] - <== Total: 1
2020-09-25 15:08:14,030 INFO [LocalCacheTest] - 第三次查询结果:UserInfo(id=1, name=赵一, age=21)
2020-09-25 15:08:14,030 INFO [LocalCacheTest] - 第四次查询结果:UserInfo(id=1, name=赵一, age=21)
2020-09-25 15:08:14,030 INFO [LocalCacheTest] - 第三次查询结果与第四次查询是否相同:true
通过控制台日志输出信息可以发现,在第一次查询与第二次查询,其传递的具体查询参数为基本数据类型,并且一致,那么在第二次查询数据时,其没有查询数据库,而是在缓存中读取第一次查询的结果;在第三次查询与第四次查询时,在方法参数中分别传递的不同的Map类型参数,其id的值相同,在结果中发现,其第四次查询没有查询数据库,所以MySql一级缓存读取和方法中的参数无关,而是和具体的查询SQL中传递的参数有关。
1.2.3 分页参数判定
示例代码:
@Test
public void pageTest() throws IOException {
SqlSession sqlSession = this.getSqlSession();
RowBounds rowBounds1 = new RowBounds(0,1);
List<UserInfo> list1 = sqlSession.selectList("org.yoko.com.dao.UserInfoDao.list", null, rowBounds1);
logger.info("第一次查询结果:" + list1);
RowBounds rowBounds2 = new RowBounds(0,2);
List<UserInfo> list2 = sqlSession.selectList("org.yoko.com.dao.UserInfoDao.list", null, rowBounds2);
logger.info("第二次查询结果:" + list2);
logger.info("第一次查询结果与第二次查询结果是否相同:" + (list1 == list2));
RowBounds rowBounds3 = new RowBounds(0,1);
List<UserInfo> list3 = sqlSession.selectList("org.yoko.com.dao.UserInfoDao.list", null, rowBounds3);
logger.info("第三次查询结果:" + list3);
logger.info("第三次查询结果与第一次查询结果是否相同:" + (list1 == list3));
}
控制台日志打印:
2020-09-25 15:47:40,090 DEBUG [org.yoko.com.dao.UserInfoDao.list] - ==> Preparing: select id, name, age from user_info where 1=1
2020-09-25 15:47:40,128 DEBUG [org.yoko.com.dao.UserInfoDao.list] - ==> Parameters:
2020-09-25 15:47:40,151 INFO [LocalCacheTest] - 第一次查询结果:[UserInfo(id=1, name=赵一, age=21)]
2020-09-25 15:47:40,153 DEBUG [org.yoko.com.dao.UserInfoDao.list] - ==> Preparing: select id, name, age from user_info where 1=1
2020-09-25 15:47:40,153 DEBUG [org.yoko.com.dao.UserInfoDao.list] - ==> Parameters:
2020-09-25 15:47:40,155 INFO [LocalCacheTest] - 第二次查询结果:[UserInfo(id=1, name=赵一, age=21), UserInfo(id=2, name=钱二, age=22)]
2020-09-25 15:47:40,155 INFO [LocalCacheTest] - 第一次查询结果与第二次查询结果是否相同:false
2020-09-25 15:47:40,157 INFO [LocalCacheTest] - 第三次查询结果:[UserInfo(id=1, name=赵一, age=21)]
2020-09-25 15:47:40,157 INFO [LocalCacheTest] - 第三次查询结果与第一次查询结果是否相同:true
MyBatis分页查询,在调用查询方法时,会传递RowBounds 类型的参数,三次查询都会创建RowBounds 对象,第一次查询与查询第二次查询RowBounds 对象内的数据不同,第一次与第三次查询RowBounds 对象内的数据相同,通过日志可以发现,虽然三次创建的RowBounds对象不同,但是如果其内部数据相同,那么后面的查询依然可以命中缓存。
1.2.4 根据具体的SQL语句判定
示例代码:
@Test
public void sqlTest() throws IOException {
SqlSession sqlSession = this.getSqlSession();
Map<String, Object> param1 = new HashMap<String, Object>();
param1.put("id", 1);
param1.put("type", 1);
UserInfo userInfo1 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById4", param1);
logger.info("第一次查询结果: " + userInfo1);
Map<String, Object> param2 = new HashMap<String, Object>();
param2.put("id", 1);
param2.put("type", 2);
UserInfo userInfo2 = sqlSession.selectOne("org.yoko.com.dao.UserInfoDao.queryById4", param2);
logger.info("第二次查询结果: " +userInfo2);
logger.info("第一次查询结果与第二次查询结果是否相同:" + (userInfo1 == userInfo2));
}
控制台日志打印:
2020-09-25 16:09:06,849 DEBUG [org.yoko.com.dao.UserInfoDao.queryById4] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 16:09:06,889 DEBUG [org.yoko.com.dao.UserInfoDao.queryById4] - ==> Parameters: 1(Integer)
2020-09-25 16:09:06,924 DEBUG [org.yoko.com.dao.UserInfoDao.queryById4] - <== Total: 1
2020-09-25 16:09:06,925 INFO [LocalCacheTest] - 第一次查询结果: UserInfo(id=1, name=赵一, age=21)
2020-09-25 16:09:06,925 DEBUG [org.yoko.com.dao.UserInfoDao.queryById4] - ==> Preparing: select id, name, age from user_info where 1=1 and id = ?
2020-09-25 16:09:06,925 DEBUG [org.yoko.com.dao.UserInfoDao.queryById4] - ==> Parameters: 1(Integer)
2020-09-25 16:09:06,930 DEBUG [org.yoko.com.dao.UserInfoDao.queryById4] - <== Total: 1
2020-09-25 16:09:06,930 INFO [LocalCacheTest] - 第二次查询结果: UserInfo(id=1, name=赵一, age=21)
2020-09-25 16:09:06,932 INFO [LocalCacheTest] - 第一次查询结果与第二次查询结果是否相同:false
在mapper.xml中queryById4方法中,通过条件执行不同的SQL,两者的区别只有在where条件中“1=1”的不同。虽然两次查询的statementId相同,并且其具体查询的SQL中传递的参数也相同,但是其两者的SQL语句是存在差异的。根据日志打印可以发现,第二次查询是没有命中缓存的。
1.2.5 环境判定,即选择的是那个数据源
在config.xml配置文件中,配置两种数据源,其访问的具体数据库相同,但那是其enviroment标签的ID不同
<!--环境配置,连接的数据库,这里使用的是MySQL-->
<environments default="mysql">
<environment id="mysql">
<!--指定事务管理的类型,这里简单使用Java的JDBC的提交和回滚设置-->
<transactionManager type="JDBC"/>
<!--dataSource 指连接源配置,POOLED是JDBC连接对象的数据源连接池的实现-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/yxs_example"/>
<property name="username" value="root"/>
<property name="password" value="yxs5516"/>
</dataSource>
</environment>
<environment id="mysql1">
<!--指定事务管理的类型,这里简单使用Java的JDBC的提交和回滚设置-->
<transactionManager type="JDBC"/>
<!--dataSource 指连接源配置,POOLED是JDBC连接对象的数据源连接池的实现-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/yxs_example"/>
<property name="username" value="root"/>
<property name="password" value="yxs5516"/>
</dataSource>
</environment>
</environments>
示例代码;
@Test
public void environmentTest() throws IOException {
// 默认环境
SqlSession sqlSession1 = this.getSqlSession();
UserInfo userInfo1 = sqlSession1.selectOne("org.yoko.com.dao.UserInfoDao.queryById1", 1);
logger.info("第一次查询结果: " + userInfo1);
// 自选环境
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "mysql1");
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserInfo userInfo2 = sqlSession2.selectOne("org.yoko.com.dao.UserInfoDao.queryById1", 1);
logger.info("第二次查询结果: " + userInfo2);
logger.info("第一次查询结果与第二次查询结果是否相同:" + (userInfo1 == userInfo2));
}
控制台日志打印:
2020-09-25 16:36:50,959 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 16:36:51,007 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Parameters: 1(Integer)
2020-09-25 16:36:51,058 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - <== Total: 1
2020-09-25 16:36:51,059 INFO [LocalCacheTest] - 第一次查询结果: UserInfo(id=1, name=赵一, age=21)
2020-09-25 16:36:51,157 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Preparing: select id, name, age from user_info where id = ?
2020-09-25 16:36:51,158 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - ==> Parameters: 1(Integer)
2020-09-25 16:36:51,160 DEBUG [org.yoko.com.dao.UserInfoDao.queryById1] - <== Total: 1
2020-09-25 16:36:51,161 INFO [LocalCacheTest] - 第二次查询结果: UserInfo(id=1, name=赵一, age=21)
2020-09-25 16:36:51,162 INFO [LocalCacheTest] - 第一次查询结果与第二次查询结果是否相同:false
在示例代码中,配置了两种数据源,访问的是同一数据库,通过日志打印可以发现,虽然调用的是同一个方法,传递相同参数,但是没有命中缓存,其实,本质上就是创建了不同的SqlSession。
总结:
已SqlSessiond对象的selectOne()方法为例,通过底层代码定位及UML类图,可以发现MyBatis一级缓存存储的是HashMap类型的数据。在BaseExecutor类中的createCacheKey()方法中确定了MyBatis一级缓存的命中原则,而在该类的query()方法中可以了解具体的查询方式,是在DB中获取数据,还是在一级缓存中获取数据。