MYBATIS事务内的查询缓存处理

前言:mybatis的查询使用的sqlSession类主要为:DefaultSqlSession.java,在这个类里提供了selectOne,selectList,insert,update,delete,select,selectMap之类的dml通用方法以及commit、rollback这类的事务控制方法。

这里今天主要讲述dml方法涉及到对应的事务内缓存,首先我们先看看selectOne方法

public <T> T selectOne(String statement) {
     
return this.<T>selectOne(statement, null);
}

public <T> T selectOne(String statement, Object parameter) {
     
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
     
return list.get(0);
} else if (list.size() > 1) {
     
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
     
return null;
}
}

从上可以看出selectOne最终都是调用的selectList,接下来看selectList

public <E> List<E> selectList(String statement) {
     
return this.selectList(statement, null);
}

public <E> List<E> selectList(String statement, Object parameter) {
     
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
     
try {
     
MappedStatement ms = configuration.getMappedStatement(statement);
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
     
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
     
ErrorContext.instance().reset();
}
}

selectList方法最终是要调用到 selectList(String statement, Object parameter, RowBounds rowBounds) 方法。在这里它会调用 org.apache.ibatis.executor.Executor 类的实现类的 query方法进行查询相关的后续逻辑。这里先看看对应query方法

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
     
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
     
Cache cache = ms.getCache();
if (cache != null) {
     
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
     
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
     
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

在这里可以看到会根据sql、sql对应的参数创建一个CacheKey。第一个query方法会调用第二个,然后先会从MappedStatement中获取cache,这里先不关注这个,因为只有设置了useCache的时候才有可能会有cache,而本次讨论查询缓存不是这个cache。接下来就要调用BaseExecutor.query方法

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
     
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
if (queryStack == 0 && ms.isFlushCacheRequired()) {
     
clearLocalCache();
}
List<E> list;
try {
     
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
     
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
     
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
     
queryStack--;
}
if (queryStack == 0) {
     
for (DeferredLoad deferredLoad : deferredLoads) {
     
deferredLoad.load();
}
deferredLoads.clear(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
     
clearLocalCache(); // issue #482
}
}
return list;
}
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

上面这一行是从localcache中获取事务中的查询缓存,如果有就返回对应的缓存,没有则会去数据库里查询,这时就进入了queryFromDatabase方法了

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
     
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
     
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
     
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
     
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

从这里可以看出会将查询结果放入localCache中,并返回结果。

从上面可以看出,如果两次调用同一条查询语句并且查询条件一致,那么就只会查询一次,第二次会从localCache中获取。那么如果第一次查询后有insert、update、delete、操作时,第二次相同查询还会从localCache中获取吗?

我们再来看看DefaultSqlSession类中的insert、update、delete方法

public int insert(String statement) {
     
return insert(statement, null);
}

public int insert(String statement, Object parameter) {
     
return update(statement, parameter);
}

public int update(String statement) {
     
return update(statement, null);
}

public int update(String statement, Object parameter) {
     
try {
     
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
     
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
     
ErrorContext.instance().reset();
}
}

public int delete(String statement) {
     
return update(statement, null);
}

public int delete(String statement, Object parameter) {
     
return update(statement, parameter);
}

可以看出来最终都是调用的 update(String statement, Object parameter) 方法,这样很好,我们就只要关注这个方法的执行了。从configuration中获取对应mybatis以及我们自己要调用的dml方法的相关配置后,就会执行executor.update 方法。现在,我们进入这个方法看看

public int update(MappedStatement ms, Object parameter) throws SQLException {
     
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
clearLocalCache();
return doUpdate(ms, parameter);
}

从这可以看出来这里会调用clearLocalCache()方法后,再执行doUpdate方法去更新数据。我们来看看clearLocalCache()方法到底干了些什么?

public void clearLocalCache() {
     
if (!closed) {
     
localCache.clear();
localOutputParameterCache.clear();
}
}

很简单,就是清理localCache中的缓存。也就是即使我们在同一个事务中使用了两次完全相同的查询,如果在这两次查询中有update、delete、insert操作,那么第二次查询会直接查询数据库,而不会从localCache中获取,而如果没有,那么就会从localCache中获取。因为这个原因,我们需要在写代码时注意到,在第一次查询后得到结果集,如果结果集这时候我们人为修改了,那么就会在第二次查询时会获取到我们修改后的结果集,而不是正确的数据。

你可能感兴趣的:(java知识提升汇总专栏,java,mybatis,sqlSession类,DefaultSql,dml通用方法)