Mybatis 源码(四)Mybatis Excuter框架
我们在上一章介绍到,Mybatis会将所有数据库操作转换成iBatis编程模型,通过门面类SqlSession来操作数据库,但是我们深入SqlSession源码我们会发现,SqlSession啥都没干,它将数据库操作都委托给你了Excuter,如图:
SqlSession嵌套图.png Executor接口定义.pngExcuter框架类图
Executor.pngBaseExecutor
在BaseExecutor定义了Executor的基本实现,如查询一级缓存,事务处理等不变的部分,操作数据库等变化部分由子类实现,使用了模板设计模式,下面我们来看下查询方法的源码:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
/**
* 所有的查询操作最后都是由该方法来处理的
*/
@SuppressWarnings("unchecked")
@Override
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) {
// 处理存储过程的OUT参数
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
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;
}
BaseExecutor查询方法流程.png
queryFromDatabase() 方法中,我们可以看到doQuery使用的是模板方法,具体逻辑是由子类来实现的,这样做的好处是,子类只关心程序变化的部分,其他不变的部分由父类实现。提高了代码的复用性和代码的扩展性。
SimpleExecutor
普通的执行器,Mybatis的默认使用该执行器,每次新建Statement。我们还是来看下查询方法的源码:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 获取Mybatis配置类
Configuration configuration = ms.getConfiguration();
// 根据配置类获取StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建Statement
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 获取Connection连接
Connection connection = getConnection(statementLog);
// 根据Connection获取Statement
stmt = handler.prepare(connection, transaction.getTimeout());
// 设置参数
handler.parameterize(stmt);
return stmt;
}
通过
stmt = handler.prepare(connection, transaction.getTimeout());
方法我们可以看出每次是新建Statement
。
ReuseExecutor
可以重用的执行器,复用的是Statement,内部以sql语句为key使用一个Map将Statement对象缓存起来,只要连接不断开,那么Statement就可以重用。
因为每一个新的SqlSession都有一个新的Executor对象,所以我们缓存在ReuseExecutor上的Statement的作用域是同一个SqlSession,所以其实这个缓存用处其实并不大。我们直接看下获取Statement
源码,其他部分和SimpleExecutor
查询方法一样。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
if (hasStatementFor(sql)) {
// 获取复用的Statement
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
// 新建Statement,并缓存
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
private boolean hasStatementFor(String sql) {
try {
// 根据sql判断是否缓存了Statement,并判断Connection是否关闭
return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
} catch (SQLException e) {
return false;
}
}
private Statement getStatement(String s) {
return statementMap.get(s);
}
private void putStatement(String sql, Statement stmt) {
statementMap.put(sql, stmt);
}
ReuseExecutor.png
BatchExecutor
批处理执行器,通过封装jdbc的 statement.addBatch(String sql) 以及 statement.executeBatch(); 来实现的批处理。该执行器的事务只能是手动提交模式。
我们平时执行批量的处理是一般还可以使用sql拼接的方式。
执行批量更新时建议一次不要更新太多数据,如果更新数据量比较大时可以分段执行。
CachingExecutor
如果开启了二级缓存那么Mybatis会使用CachingExecutor
执行器,CachingExecutor
使用了装饰器模式。
@Override
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, boundSql);
// 查缓存
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 调用被装饰则的方法
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将数据放入缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没找到缓存,直接调用被装饰则的方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
通过源码我们发现,整个查询流程变成 了 L2 -> L1 -> DB。CachingExecutor
的查询流程,增加了二级缓存的查询操作。
我们在实际使用缓存过程中一般很少使用Mybatis的二级缓存,如果想做二级缓存,建议直接在service层面使用第三方缓存框架,推荐使用为监控而生的多级缓存框架 layering-cache,使用更方便灵活,查询流程是 L1 -> L2 -> DB。
总结
- BaseExecutor:使用了模板方法模式,定义了Executor的基本实现,它是一个抽象类,不能直接对外提供服务。
- SimpleExecutor:普通的执行器,Mybatis的默认使用该执行器,每次新建Statement。
- ReuseExecutor:可以重用Statement的执行器,但是这个Statement缓存只在一次SqlSession中有效,我们平时生少有在一次SqlSession中进行多次一样的查询操作,所以性能提升并不大。
- BatchExecutor:批处理执行器
- CachingExecutor:二级缓存执行器,使用装饰器模式,整个查询流程变成 了 L2 -> L1 -> DB。建议直接使用第三方缓存框架,如:为监控而生的多级缓存框架 layering-cache。