MyBatis缓存学习(一)

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中获取数据,还是在一级缓存中获取数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值