功能
在 mybatis 中执行语句时,有个非常重要的类就是 Executor,Executor 有些类似是mybatis的心脏,它负责这次语句执行操作的资源调度、流程执行等功能。Executor是一个接口,它有几个实现类,在前边sqlsession构建时已经分析过,mybatis默认的策略是 SimpleExecutor ,它完全继承自 BaseExecutor,没有特殊的地方。
Execute所要完成的工作:
- 根据当前配置的id找到MappedStatement,然后处理sqlSource拿到BoundSource,在这里会顺便去检查一下paramterMapping是不是正确如果有的话。
- 创建一个 CacheKey,CacheKey的作用是作为缓存的key,既然要能成为key就得保证唯一,CacheKey会将你当前要操作的关于sql的一些重要信息以栈(list)的方式存储到CacheKey中来保证唯一,在之后的流程执行的时候会一个个弹出来,可以保证顺序,这是一个很巧妙的设计,通过这种方式保证每个重要参数都不会丢失并且还能保证执行顺序。
- 执行sql语句,以query来讲,执行的时候会去判断有没有开启二级缓存,然后有没有允许每次执行刷新清空缓存,然后判断一级缓存中是否缓存(一级缓存是默认开启的,每次查询都会强制缓存一份结果,有策略控制),然后就是准备statement语句调用驱动去执行,封装返回结果。
UML
Executor在mybatis中有三个实现类,baseExecutor是一个基类,包含一些共有的功能。
- SimpleExecutor 不做任何处理,普通的执行器
- ReuseExecutor 复用已经处理好的语句,也就是prepareStatement,在本次会话中再次调用时不会再去解析,有些场景下可以考虑使用这个节省性能
- BatchExecutor 允许批量操作,批量场景多的情况下可以考虑使用这个
CachingExecutor 是一个封装执行器,类似装饰者模式,在原有的模式上封装了一些功能,二级缓存就是在 CachingExecutor 中控住的。
代码详解
DefaultSqlSession的selectOne方法内部其实是很简单的,它将MappedStatement的 id 以及入参传入进来,在内部会去调用selectList方法,结果只取第一个元素,如果返回的结果超过1则会报错,这个错误大家也熟悉,是经常见的一个错误,就是这里报出来的。
User user = sqlSession.selectOne("org.apache.ibatis.test.UserMapper.selectById", 1);
// selectOne方法,在内部调用selectList方法
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.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;
}
}
selectList() 方法中继续调用一个重载方法,在这里有一个类是RowBounds,这个类是mybatis的默认分页,内部有offset以及limit等属性来控制分页,limit的默认值是Integer的最大值
selectList 重载方法中会根据你传入进来的id 去configuration中获取到当前的MappedStatement,然后调用executor去执行query()方法,这里的executor其实是CachingExecutor ,这个在sqlsession的获取时说明过。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor 的方法逻辑中分这几步,第一步是获取 BoundSql ,第二部是根据查询条件构造 CacheKey ,第三步是执行query方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 第一步获取BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 第二步获取 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 第三步查询
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Boundsql是sql语句在执行时的最终状态,sqlSource是mybatis在解析过程中可以存储到configuration中的形态,到了执行时会在这里将sql解析成为Boundsql,在Boundsql中有sql(调用sqlNode的apply方法解析后的sql),入参mapping,parameterObject(入参参数),metaParameters(反射工具类)这些信息,方便在之后的执行时使用。
getBoundSql()方法中,主要逻辑在 DynamicSqlSource.getBoundSql() 方法中,在其内会去循环调用所有SqlNode的apply()方法解析sql,包括 $ {}这种替换成相应的值,$ {}这种替换是在apply()中完成的,之前是根据配置属性,这里是根据入参。
再解析完sqlNode之后这里的sql已经全部都是静态文本了,之后会去重新构建解析一次sqlSource,防止之前有些没有替换掉的 # {} 处理完,这里的解析肯定会返回staticSqlSource类型,再次调用getBoundSql就会拿到BoundSql
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
/.../
return boundSql;
}
// sqlSource.getBoundSql()
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 执行各个sqlNode的apply方法去解析动态Sql,循环调用
rootSqlNode.apply(context);
// SqlSourceBuilder,主要是替换 #{}占位符,
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 调用 staticSource()的方法
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
rootSqlNode.apply(context) 这个方法其实是调用的MixedSqlNode.apply方法,之前解析的时候应该已经说明,在内部可以看到会去循环调用apply()方法
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
之后SqlSource的parse()与之前的parse方法一致,不多做介绍,在getBoundSql()方法中其实也是很简单,就是同构构造方法构造了一个BoundSql
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
回到 CachingExecutor 的query方法中,获取到BoundSql 之后就是构造CacheKey,缓存的唯一key,具体逻辑是在 BaseExecutor 类的 createCacheKey 方法中,他会将当前MappedStatement的id,分页逻辑的起始位置,最大长度以及sql都以类似栈(List)结构的形势存储起来并且根据算法计算一个hash值
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// id
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// sql
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// 不配置parameterMapping的话这段逻辑没什么用
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
// 当前环境的id也会塞进去
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
update方法内部是一个算法,会去计算hashcode并且与之前的相乘,到时候取的时候则根据hashcode一个个逆向回来,保证每个参数都会被用到,这种设计的方式其实还是很巧妙的。
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
之后就是重载的query方法了,这里有个二级缓存的逻辑需要注意一下,首先会获取当前statmentt的cache,如果有的话就去看是否配置了使用缓存,如果配置了则会去查询缓存,如果没有则走db,并且将查询的结果塞到 TransactionalCacheManager 这个类中,TransactionalCacheManager 就是保管缓存的一个类。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 二级缓存,会获取当前statmentt的cache,如果有的话就去看是否配置了使用缓存,如果配置了则继续
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);
}
delegate.query() 方法其实调用的是 BaseExecutor 类的query,这里本来应该是SimpleExecutor,但是因为SimpleExecutor什么都没有做完全继承自BaseExecutor ,所以实际的执行者还是BaseExecutor ,在query方法中,首先会根据配置确定是否要清除localCache 中的缓存,然后就是执行查询,最后会根据你配置的localcache的缓存策略去判断是否保留本地缓存。
mybatis默认是开启着一个本地缓存的,也就是一级缓存,它会将查询的结果默认塞到这个本地缓存中,下一次来查询的时候就可以从这个缓存中获取结果,这个逻辑是必须要走的,不过是有策略可以控制这个缓存的生效策略。
在configuration中可以配置LocalCacheScope,有两种:
SESSION 本次会话内都会保留缓存
STATEMENT 每次语句执行完就清除掉
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 如果需要清理缓存则需要清理缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// 查询计数器,queryStack是保证递归的时候全部结束掉再处理是否要清楚 localCache 中的一级缓存
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();
}
// 会根据LocalCacheScope缓存策略去判断是否清楚本地缓存
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
queryFromDatabase方法中,是通过环绕的方式将localCache中设值,具体的查询方法是在doQuery中
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);
// 三种执行语句,如果是 CALLABLE 语句则会往 localOutputParameterCache 设置一份
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
doQuery方法是个抽象方法,具体的实现逻辑是在各自的实现类中,这里是在SimpleExecutor中,它的逻辑也很简单,就是构建StatementHandler ,构建mysql的statement,然后通过handler去执行。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 构建StatemenHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 构造statment
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行statement
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
查询数据并且封装结果是在StatementHandler 中完成,Executor负责的资源调度任务到这里就已经结束了。