我们都知道Mybatis默认开启了一级缓存,二级缓存需要手动开启。我自己最近也在复习Mybatis相关的知识。但是在写的时候,发现一级缓存并没有生效,因此特此记录。
首先可以看这篇文章Mybatis - 预编译的运用和原理搭建一个项目。
我们对Controller层的代码做出更改,就是调用一次接口,我们会调用数据库两次:
@PostMapping("/hello")
public User hello(){
userProcess.getUser("tom");
return userProcess.getUser("tom");
}
调用一次接口之后,控制台输出:

可以发现,这里依旧是执行了两次SQL,可见一级缓存并没有生效,那是咋回事呐?
首先我们应该知道的一点是,Mybatis的一级缓存是通过SqlSession来实现的。那么SqlSession在Mybatis中是个什么样的存在呢?他是一个接口:
public interface SqlSession extends Closeable {}
对应的实现子类有:

同时Mybatis官网写到这么一句话:

翻译一下就是:
MyBatis中,使用sqlsessionFactory创建一个sqlsession。一旦有了会话,就可以使用它来执行Mapper的映射语句、提交或回滚连接,最后,当不再需要时,关闭会话。mybatis-spring,就不需要直接使用sqlsessionFactory,因为可以向bean注入一个线程安全的sqlsession,它根据spring的事务配置自动提交、回滚和关闭会话。Mybatis中如果使用了一级缓存,默认的实现是DefaultSqlSession。但是结合上面的文档来看,在Spring整合Mybatis的时候,就会有另一套逻辑。这里的具体实现是SqlSessionTemplate。
也就是说,当Spring-Mybatis整合的时候,Spring对SqlSession的使用是通过SqlSessionTemplate来控制的。我们看下SqlSessionTemplate这个类的构造函数。
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 关键在于这里
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
我们可以看到SqlSession这个实例是通过JDK的动态代理来创建出的代理对象。 动态代理的相关文章如果想看的可以看这里:
既然他是一个代理对象了,那么我们需要关注的逻辑肯定是它代理的逻辑喽,那就得看SqlSessionInterceptor这个拦截器里面加了啥操作。
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1.创建一个SqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 2.调用原始函数
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// 如果不是事务,关闭当前的SqlSession
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
// ...catch
} finally {
// 关闭SqlSession
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
总结下就是:
SqlSession。SqlSession。因此结合本文的案例来看就是:
SQL,Mybatis默认开启了一级缓存,因此理论上来看,应该只有第一次是走了SQL。后续都是走缓存才对。但是实际上却每次都执行了SQL。Mybatis的一级缓存是通过SqlSession来实现的,对应的实现是DefaultSqlSession。(package org.apache.ibatis.session.defaults;)Spring管理了Mybatis,因此这个时候SqlSession的实现被替换成了SqlSessionTemplate。(package org.mybatis.spring;)注意观察包名。SqlSessionTemplate中主要是通过JDK动态代理创建出了一个SqlSession代理对象。增强的逻辑如上。SQL相同,Spring都会创建一个新的SqlSession,并且在执行完毕之后,还会将事务提交、关闭SqlSession。因此多次请求之间无法通过SqlSession来共享缓存。SQL、缓存失效的现象。从源码上分析来看,想要Spring-Mybatis项目中实现一级缓存,那么就得通过事务声明来完成。我们可以对Controller代码这么更改(这只是案例,实际项目中建议不要在Controller层添加事务声明,粒度太大!):
@PostMapping("/hello")
@Transactional
public User hello(){
userProcess.getUser("tom");
return userProcess.getUser("tom");
}
再次访问,可以见到第一次调用的时候,执行了SQL,后续执行了缓存。

并且从输出结果上来看,也吻合源码的操作,我们给对应的代码块添加了事务的声明,因此这里在判断的时候,就会返回true。因此不会讲当前的SqlSession提交。那么因此SqlSession能做到共享(没有被提交)。

到这里,一级缓存失效的问题也就解决啦。