我们都知道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
能做到共享(没有被提交)。
到这里,一级缓存失效的问题也就解决啦。