一级缓存

先通过一个简单示例来看看 MyBatis 一级缓存如何起作用。在 src.mybatis.simple.mapper 包下,新建如下测试类。

public class CacheTest extends BaseMapperTest {
    @Test
    public void testL1Cache() {
        //获取 sqlSession
        SqlSession sqlSession = getSqlSession();
        SysUser user1 = null;
        try {
            //获取 UserMapper 接口
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //调用 selectById 方法,查询 id = 1 的用户
            user1 = userMapper.selectById(1l);
            //对当前获取的对象重新赋值
            user1.setUserName("New Name");
            //再次查询获取 id 相同的用户
            SysUser user2 = userMapper.selectById(1l);
            //虽然我们没有更新数据库,但是这个用户名和我们 user1 重新赋值的名字相同了
            Assert.assertEquals("New Name", user2.getUserName());
            //不仅如何,user2 和 user1 完全就是同一个实例
            Assert.assertEquals(user1, user2);
        } finally {
            //关闭当前的 sqlSession
            sqlSession.close();
        }
        System.out.println("开启新的 sqlSession");
        //开始另一个新的 session
        sqlSession = getSqlSession();
        try {
            //获取 UserMapper 接口
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //调用 selectById 方法,查询 id = 1 的用户
            SysUser user2 = userMapper.selectById(1l);
            //第二个 session 获取的用户名仍然是 admin
            Assert.assertNotEquals("New Name", user2.getUserName());
            //这里的 user2 和 前一个 session 查询的结果是两个不同的实例
            Assert.assertNotEquals(user1, user2);
            //执行删除操作
            userMapper.deleteById(2L);
            //获取 user3
            SysUser user3 = userMapper.selectById(1l);
            //这里的 user2 和 user3 是两个不同的实例
            Assert.assertNotEquals(user2, user3);
        } finally {
            //关闭 sqlSession
            sqlSession.close();
        }
    }
}

先执行该测试输出日志,然后结合日志一起来看以上代码。输出日志如下。

在第一次执行 selectById 方法获取 SysUser 数据时,真正执行了数据库查询,得到了 user1 的结果。第二次执行获取 user2 的时候,从日志可以看到,在 “开启新的 sqlSession” 这行日志上面,只有一次查询,也就是说第二次查询并没有执行数据库操作。

从测试代码来看,获取 user1 后重新设置了 userName 的值,之后没有进行任何更新数据库的操作。在获取 user2 对象后,发现 user2 对象的 userName 值竟然和 user1 重新设置后的值一样。再往下可以发现,原来 user1 和 user2 竟然是同一个对象,之所以这样就是因为 MyBatis 的一级缓存。

MyBatis 的一级缓存存在于 SqlSession 的生命周期中,在同一个 SqlSession 中查询时,MyBatis 会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个 Map 对象中。如果同一个 SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当 Map 缓存对象中已经存在该键值时,则会返回缓存中的对象。

缓存中的对象和我们得到的结果是同一个对象,反复使用相同参数执行同一个方法时,总是返回同一个对象,因此就会出现上面测试代码中的情况。在使用 MyBatis 的过程中,要避免在使用如上代码中的 user2 时出现的错误。我们可能以为获取的 user2 应该是数据库中的数据,却不知道 user1 的一个重新赋值会影响到 user2。如果不想让 selectById 方法使用一级缓存,可以对该方法做如下修改。

<select id="selectByid" flushCache="true" resultMap="userMap">
    select * from sys_user where id=#{id}
</select>

该修改在原来方法的基础上增加了 flushCache="true",这个属性配置为 true 后,会在查询数据前清空当前的一级缓存,因此该方法每次都会重新从数据库中查询数据,此时的 user2 和 user1 就会成为两个不同的实例,可以避免上面的问题。但是由于这个方法清空了一级缓存,会影响当前 SqlSession 中所有缓存的查询,因此在需要反复查询获取只读数据的情况下,会增加数据库的查询次数,所以要避免这么使用。

在关闭第一个 SqlSession 后,又重新获取了一个 SqlSession,因此又重新查询了 user2,这时在日志中输出了数据库查询 SQL,user2 是一个新的实例,和 user1 没有任何关系。这是因为一级缓存是和 SqlSession 绑定的,只存在于 SqlSession 的生命周期中。

接下来执行了一个 deleteById 操作,然后使用相同的方法和参数获取了 user3 实例,从日志和结果来看,user3 和 user2 也是完全不同的两个对象。这是因为任何的 INSERT、UPDATE、DELETE 操作都会清空一级缓存,所以查询 user3 的时候由于缓存不存在,就会再次执行数据库查询获取数据。

关于一级缓存中的各种情况,通过上面的测试都已经介绍完了,由于一级缓存是在默默地工作,因此要避免在使用过程中由于不了解而发生觉察不到的错误。