- ReuseExecutor
ReuseExecutor 提供了对 Statement 对象重用的机制,以减少该对象创建和销毁,以及 SQL 预编译所带来的开销。ReuseExecutor 类中定义了一个ReuseExecutor#statementMap
属性(如下),其中 key 为 SQL 语句,value 为对应的 Statement 对象,以此实现对 Statement 对象的复用。
/** 缓存 Statement 对象,key 为对应的 SQL 语句(带有 ? 占位符) */
private final Map<String, Statement> statementMap = new HashMap<String, Statement>();
ReuseExecutor 中的方法实现也基本上沿用了同一套思路,仍然以ReuseExecutor#doQuery
为例进行说明,该方法实现如下:
public <E> List<E> doQuery(MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
// 创建对应的 StatementHandler 对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 先尝试从缓存中获取当前 SQL 对应的 Statement 对象,缓存不命中则创建一个新的并缓存
Statement stmt = this.prepareStatement(handler, ms.getStatementLog());
// 执行数据库查询操作,以及结果集映射
return handler.query(stmt, resultHandler);
}
上述方法与SimpleExecutor#doQuery
的区别在于在获取 Statement 对象时会先尝试从本地缓存中获取,如果缓存不命中则会创建一个新的 Statement 对象,并更新缓存,实现位于ReuseExecutor#prepareStatement
方法中:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 获取缓存的 Statement 对象
if (this.hasStatementFor(sql)) {
stmt = this.getStatement(sql);
this.applyTransactionTimeout(stmt);
}
// 缓存不命中,新建一个 Statement 对象并缓存
else {
Connection connection = this.getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
this.putStatement(sql, stmt);
}
// 绑定实参
handler.parameterize(stmt);
return stmt;
}
BatchExecutor
BatchExecutor 用于批量执行 SQL 语句。通常应用程序都是单行的执行 SQL 语句,但是某些场景下单行执行数据库操作是比较耗时的,比如需要远程执行数据库操作。因此,JDBC 针对 INSERT、UPDATE,以及 DELETE 操作提供了批量执行的支持。
BatchExecutor 是批量 SQL 语句执行器,其属性定义如下:
/** 缓存多个 {@link Statement} 对象,每个对象都对应多条 SQL 语句 */
private final List<Statement> statementList = new ArrayList<>();
/** 记录批处理的结果,每个 {@link BatchResult} 对应一个 {@link Statement} 对象 */
private final List<BatchResult> batchResultList = new ArrayList<>();
/** 当前执行的 SQL 语句 */
private String currentSql;
/** 当前操作的 {@link MappedStatement} 对象 */
private MappedStatement currentStatement;
下面探究一下 BatchExecutor 的批处理执行过程。首先来看一下BatchExecutor#doUpdate
方法实现,该方法用于添加批处理 SQL 语句:
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
// 基于参数创建对应的 StatementHandler 对象
final StatementHandler handler = configuration.newStatementHandler(
this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
// 当前执行的 SQL 语句与前一次执行的 SQL 语句(不包含实参)相同,且对应的 MappedStatement 对象也相同
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 获取缓存的最后一个 Statement 对象,即前一次使用的 Statement 对象
int last = statementList.size() - 1;
stmt = statementList.get(last);
this.applyTransactionTimeout(stmt);
// 绑定实参
handler.parameterize(stmt);//fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
}
// 当前执行的 SQL 语句与前一次执行的 SQL 语句不同
else {
Connection connection = this.getConnection(ms.getStatementLog());
// 获取一个新的 Statement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 绑定实参
handler.parameterize(stmt); //fix Issues 322
// 记录本次执行的 SQL 语句和 MappedStatement 对象
currentSql = sql;
currentStatement = ms;
// 缓存新建的 Statement 对象
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 基于 Statement#addBatch 方法添加批量 SQL 语句
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
上述方法中会判断当前执行的 SQL 模式(包含 ?
占位符的 SQL 语句)是否与前一次执行的相同,如果相同就会获取上次执行的 Statement 对象,并为之绑定实参;否则就会创建一个新的 Statement 对象,并记录本次执行的 SQL 模式,最后基于底层的数据库批处理方法 Statement#addBatch
添加批量 SQL 语句。由上述方法我们可以知道,对于连续同模式的批处理 SQL 操作会共享同一个 Statement 对象。
那么这些添加的批量 SQL 又是如何被执行的呢?这个过程位于 BatchExecutor#doFlushStatements
方法中,方法如下:
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
// 用于存储批量处理结果
List<BatchResult> results = new ArrayList<>();
if (isRollback) {
return Collections.emptyList();
}
// 遍历处理缓存的 Statement 集合
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
this.applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
// 批量执行当前 Statement 蕴含的多条 SQL 语句,并记录每条 SQL 语句影响的行数
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
// 获取数据库生成的主键,并记录到 parameterObjects 中
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
this.closeStatement(stmt);
} catch (BatchUpdateException e) {
// ... 省略异常处理
}
// 记录封装当前 Statement 对象执行结果的 batchResult 到集合中
results.add(batchResult);
}
return results;
} finally {
// 关闭所有的 Statement 对象
for (Statement stmt : statementList) {
this.closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
方法会遍历我们在 BatchExecutor#doUpdate
中构造的 Statement 集合,分别执行集合中蕴含的 Statement 对象,并将执行的结果记录到 BatchResult 对象中(说明:在 BatchExecutor#doUpdate
方法中已经为每个 Statement 对象构造好了一个空的 BatchResult 对象,记录在 BatchExecutor#batchResultList
集合中),最后将 BatchResult 对象封装到集合中返回。因为都是数据库更新一类的操作,所以这里没有复杂的结果集映射,只需要记录每一条 SQL 语句执行所影响的行数即可。
CachingExecutor
由前面 Executor 的继承关系我们可以看到,CachingExecutor 相对于其它 Executor 实现来说似乎有其特别之处。CachingExecutor 直接实现了 Executor 接口,实际上它是一个 Executor 装饰器,用于为 Executor 提供二级缓存支持。该接口的属性定义如下:
/** 装饰的 {@link Executor} 对象 */
private final Executor delegate;
/** 用于管理当前使用的二级缓存对象 */
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
其中第一个属性就是 CachingExecutor 具体修饰的 Executor 对象。我们来看一下第二个属性,TransactionalCacheManager 用来管理当前 CachingExecutor 对应的二级缓存对象,它的方法实现都比较简单,其中相对让人疑惑的是它的唯一一个属性:
/** key 为对应的 {@link CachingExecutor} 使用的二级缓存对象,value 为采用 {@link TransactionalCache} 装饰的二级缓存对象 */
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
该属性的 key 就是当前对应的二级缓存,而 value 则是对于该二级缓存对象采用 TransactionalCache 装饰后的对象。所以 key 和 value 本质上都映射到同一个缓存对象,只是 value 采用了 TransactionalCache 进行增强。TransactionalCache 也是一个缓存装饰器,在前面介绍缓存装饰器实现时特意留着没有说明,这里一起来分析一下。该装饰器的属性定义如下:
/** 被装饰的 {@link Cache} 对象(二级缓存) */
private final Cache delegate;
/** 是否在提交事务时清空缓存 */
private boolean clearOnCommit;
/** 用于缓存数据,当提交事务时会将其中的数据写入二级缓存 */
private final Map<Object, Object> entriesToAddOnCommit;
/** 缓存未命中的 key */
private final Set<Object> entriesMissedInCache;
对应的读缓存和写缓存操作,以及事务提交方法实现比较简单,读者可以自行阅读源码。
继续回来看 CachingExecutor 的实现,所有实现方法中只有 CachingExecutor#query
方法稍微复杂一些,该方法的实现如下:
public <E> List<E> query(MappedStatement ms,
Object parameterObject,
RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
// 获取对应的 BoundSql 对象,并创建对应的 CacheKey
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用重载的 query 方法
return this.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) {
// 依据配置决定是否清空二级缓存
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 确保不是存储过程输出类型的参数
this.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);
// 缓存到 TransactionalCache#entriesToAddOnCommit 中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 未启用二级缓存,则查询一级缓存,再不命中就查询数据库
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
正如我们一开始对于 MyBatis 缓存结构设计描绘的那样,上述方法首先在二级缓存中进行检索,如果二级缓存不命中则会执行被装饰的 Executor 对象的 Executor#query
方法。由前面的分析我们知道,Executor 的实现都自带一级缓存特性,所以接下去会查询一级缓存。只有在一级缓存也不命中的情况下,请求才会落库,并由数据库返回的结果对象更新一级缓存和二级缓存。
那么这里使用的二级缓存对象是在哪里创建的呢?实际上前面我们就定义说二级缓存是应用级别的,所以当应用启动时二级缓存就已经被创建了,这个过程发生在对映射文件进行解析时。在映射文件中我们会按照需要配置一定的 <cache/>
和 <cache-ref>
标签,而在解析 <cache/>
标签时会调用 MapperBuilderAssistant#useNewCache
方法创建对应的二级缓存对象。
总结
本文对 MyBatis 执行 SQL 语句所涉及到的各个方面做了一个比较详细的分析。当我们基于 MyBatis 触发一次数据库操作时,首先需要开启一次数据库会话,然后获取目标 Mapper 接口,并调用相应的 Mapper 方法执行数据库操作,最后拿到操作结果。MyBatis 在这中间基于动态代理机制实现了 SQL 语句的检索、参数绑定、数据库操作,以及结果集映射等一系列操作,并引入了缓存机制优化数据库查询性能。
回过头来看,MyBatis 的整体设计还是非常巧妙的,却也很是直观且简单,是对动态代理机制的典型应用,其设计思想和对于设计模式的应用值得我们在实际开发中借鉴。
本文是 MyBatis 源码解析系列的最后一篇文章,由于时间仓促,再加上作者水平有限,整个系列的文章中不免有错误之处,还望批评指正!