Executor是MyBatis的核心接口之一,其中定义了数据库操作的基本方法。在实际应用中经常涉及的SqlSession接口的功能,都是基于Executor接口实现的。Executor接口中定义的方法如下:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
// 执行insert、update、delete等操作
int update(MappedStatement ms, Object parameter) throws SQLException;
// 执行 select类型的SQL语句,返回位分为结果对象列表或游标对象
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
// 批量执行SQL语句
List<BatchResult> flushStatements() throws SQLException;
// 提交事务
void commit(boolean required) throws SQLException;
// 回滚事务
void rollback(boolean required) throws SQLException;
// 创建缓存中用的CacheKey对象
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 根据CacheKey对象查找缓存
boolean isCached(MappedStatement ms, CacheKey key);
// 清空一级缓存
void clearLocalCache();
// 延迟加载一级缓存中的数据
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
// 获取事务对象
Transaction getTransaction();
// 关闭Executor对象
void close(boolean forceRollback);
// 检测Executor是否已关闭
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
1.BaseExecutor
BaseExecutor是一个实现了Executor接口的抽象类,它实现了Executor接口的大部分方法,其中就使用了模板方法模式。BaseExecutor主要提供了缓存管理和事务管理的基本功能,继承BaseExecutor的子类只要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别doUpdate()方法、doQuery()方法、doQueryCursor()方法、doFlushStatement()方法,其余的功能在BaseExecutor实现。
BaseExecutor各个字段的含义如下:
// Transaction对象,实现事务的提交、回滚和关闭操作
protected Transaction transaction;
// 封装的Executor对象
protected Executor wrapper;
// 延迟加载队列
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 一级缓存,用于缓存该Executor对象查询结果集映射得到的结果对象
protected PerpetualCache localCache;
// 一级缓存,用于缓存输出类型的参数
protected PerpetualCache localOutputParameterCache;
// 用来记录嵌套查询的层数
protected int queryStack;
一级缓存
执行 select 语句查询数据库是最常用的功能,BaseExecutor.query()方法会首先创建CacheKey对象,并根据该CacheKey对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果缓存未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象。query()方法的具体实现如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建CacheKey对象
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 调用query()的一个重载
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
这里关注BaseExecutor.createCacheKey()方法创建的CacheKey对象由哪几部分构成,createCacheKey()方法具体实现如下:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
// 检测Executor是否关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// 添加MappedStatement的id
cacheKey.update(ms.getId());
// 添加offset
cacheKey.update(rowBounds.getOffset());
// 添加limit
cacheKey.update(rowBounds.getLimit());
// 添加SQL语句
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
// 获取实参并添加到CacheKey
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);
}
}
// 添加Environment的id
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
由此可知该CacheKey对象由MappedStatement的id 、对应的offset、limit、SQL语句(包含?占位符)、用户传递的实参以及Environment.id这五部分构成。
继续来看上述代码中调用的query()方法的另一重载的具体实现,该重载会根据前面创建的CacheKey对象查询一级缓存,如果缓存命中则将缓存中记录的结果对象返回,如果缓存未命中,则调用 doQuery()方法完成数据库的查询操作并得到结果对象,之后将结果对象记录到一级缓存中。具体实现如下:
@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());
// 检测Executor是否关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 非嵌套查询,并且<select>节点配置的flushCache属性为true时,才会清空一级缓存
clearLocalCache();
}
List<E> list;
try {
// 增加查询层数
queryStack++;
// 查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 针对存储过程调用的处理,其功能是:在一级缓存命中时,获取缓存中保存的输出类型参数,并设置到用户传入的实参对象中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 调用doQuery()方法完成数据库查询,并得到映射后的结果对象,
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;
}
上面介绍了BaseExecutor中缓存的第一种功能,也就是缓存结查询得到的结果对象。除此之外,一级缓存还有第二个功能:前面在分析嵌套查询时,如果一级缓存中缓存了嵌套查询的结果对象,则可以从一级缓存中直接加载该结果对象;如果一级缓存中记录的嵌套查询的结果对象并未完全加载,则可以通过DeferredLoad实现类似延迟加载的功能。
Executor中与上述功能直接相关的方法有两个,一个是isCached()方法负责检测是否缓存指定查询的结果对象,具体实现如下:
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
// 检测缓存中是否缓存了对应的对象
return localCache.getObject(key) != null;
}
另一个是 deferLoad()方法,它负责创建DeferredLoad对象并将其添加到deferredLoads集合中,具体实现如下:
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
return doQueryCursor(ms, parameter, rowBounds, boundSql);
}
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 创建DeferredLoad对象
DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);
if (deferredLoad.canLoad()) {
// 一级缓存中已经记录了指定查询的结果对象 直接从缓存中加载对象,并设置到外层对象中
deferredLoad.load();
} else {
// 将DeferredLoad对象添加到deferredLoads队列中,待整个外层查询结束后,再加载该结果对象
deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));
}
}
DeferredLoad是定义在BaseExecutor中的内部类,它负责从localCache缓存中延迟加载结果对象,其字段的含义如下:
// 外层对象对应的MetaObject对象
private final MetaObject resultObject;
// 延迟加载的局生名称
private final String property;
// 延迟加载的属性的类型
private final Class<?> targetType;
// 延迟加载的结果对象在一级缓存中相应的CacheKey对象
private final CacheKey key;
// 一级缓存
private final PerpetualCache localCache;
private final ObjectFactory objectFactory;
// 负责结采对象的类型转换
private final ResultExtractor resultExtractor;
DeferredLoad.canLoad()方法负责检测缓存项是否已经完全加载到了缓存中。首先要说明完全加载的含义BaseExecutor.queryFromDatabase()方法中,开始查询调用doQuery()方法查询数据库之前,会先在localCache中添加占位符,待查询完成之后才将真正的结果对象放到localCache中缓存,此时该缓存项才算完全加载。BaseExecutor.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;
}
DeferredLoad.canLoad()方法的具体实现如下:
public boolean canLoad() {
// 检测缓存是否存在指定的结果对象和是否为占位符
return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
DeferredLoad.load()方法负责从缓存中加载结果对象,并设置到外层对象的相应属性中,具体实现如下:
public void load() {
@SuppressWarnings("unchecked")
// we suppose we get back a List
// 从缓存中查询结果对象
List<Object> list = (List<Object>) localCache.getObject(key);
// 将结果对象转换成指定类型
Object value = resultExtractor.extractObjectFromList(list, targetType);
// 设置到外层对象的对应属性
resultObject.setValue(property, value);
}
介绍完DeferredLoad对象之后,来看触发DeferredLoad缓存中加载结果对象的相关代码,这段代码在 BaseExecutor. query()方法中,具体实现如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// ...
// 延迟加载
if (queryStack == 0) {
// 在最外层的查询结束时,所有嵌套查询也已经完成,相关缓存项也已经完全加载,所以在这里可以触发DeferredLoad加载一级缓存中记录的嵌套查询的结果对象
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 加载完成后清空deferredLoads
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
// 根据localCacheScope配置决定是否清空一级缓存
clearLocalCache();
}
}
return list;
}
BaseExecutor.queryCursor()方法的主要功能也是查询数据库,这一点与 query()方法类似,但它不会直接将结果集映射为结果对象,而是将结果集封装成Cursor对象并返回,待用户遍历Cursor时才真正完成结果集的映射操作。另外,queryCursor()方法是直接调用doQueryCursor()这个基本方法实现的,并不会像 query()方法那样使用查询一级缓存。
BaseExecutor.update()方法负责执行insert、update、delete三类SQL语句,它是调用doUpdate()模板方法实现的。在调用doUpdate()方法之前会清空缓存,因为执行SQL语句之后,数据库中的数据已经更新,一级缓存的内容与数据库中的数据可能己经不一致了,所以需要调用clearLocalCache()方法清空一级缓存中的脏数据。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
// 判断当前Executor是否已经关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清楚一级缓存
clearLocalCache();
// 调用doUpdate()方法
return doUpdate(ms, parameter);
}
事务相关操作
在BatchExecutor实现中,可以缓存多条SQL语句,等待合适的时机将缓存的多条SQL语句,并发送到数据库执行 Executor.flushStatements()方法主要是针对批处理多条SQL语句的,它会调用doFlushStatements()这个基本方法处理 Executor 缓存的多条SQL语句。在BaseExecutor.commit()、rollback()等方法中都会首先调用flushStatements()方法,然后再执行相关事务操作。
BaseExecutor. flushStatements()方法的具体实现如下:
public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
// 判断当前Executor是否已经关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 调用doFlushStatements()这个基本方法,其参数isRollBack表示是否执行Executor中缓存的SQL语句,false表示执行,true表示不执行
return doFlushStatements(isRollBack);
}
BaseExecutor.commit()方法首先会清空一级缓存、调用flushStatements()方法,最后才根据参数决定是否真正提交事务。 commit()方法的实现如下:
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清空一级缓存
clearLocalCache();
// 执行缓存的SQL语句,其中调用了flushStatements(false)方法
flushStatements();
// 根据required参数决定是否提交事务
if (required) {
transaction.commit();
}
}
BaseExecutor.rollback()方法的实现与commit()实现类似,同样会根据参数决定是否真正回滚事务 ,区别是其中调用的是flushStatements()方法的isRollBack参数为true这就会导致Executor中缓存的 SQL 语句全部被忽略,即不会被发送到数据库执行。
BaseExecutor.close()方法首先会调用rollback()方法忽略缓存的SQL语句,之后根据参数决定是否关闭底层的数据库连接。
2.SimpleExecutor
SimpleExecutor继承了BaseExecutor抽象类,它是最简单的Executor接口实现。正如前面所说,Executor使用了模板方法模式,一级缓存等固定不变的操作都封装到了BaseExecutor。SimpleExecutor中就不必再关心一级缓存等操作,只需要专注实现四个基本方法的实现即可。
首先来看SimpleExecutor.doQuery()方法的具体实现:
@Override
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();
// StatementHandler对象 ,实际返回的是RoutingStatementHandler对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 完成Statement的创建和初始化
stmt = prepareStatement(handler, ms.getStatementLog());
// 调用StatementHandler .query()方法,执行SQL语句,并通过ResultSetHandler,完成结果集的映射
return handler.query(stmt, resultHandler);
} finally {
// 关闭Statement对象
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 创建Statements对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 处理占位符
handler.parameterize(stmt);
return stmt;
}
SimpleExecutor.doQueryCursor()方法、doUpdate()方法与doQuery()方法实现类似,这里不再赘述。 SimpleExecutor不提供批量处理SQL语句的功能,所以其doFlushStatements()方法直接返回空集合,不做其他任何操作。
3.ReuseExecutor
在传统的JDBC编程中,重用Statement对象是常用的一种优化手段,该优化手段可以减少SQL预编译的开销以及创建和销毁Statement对象的开销,从而提高性能。
ReuseExecutor提供了Statement重用的功能,ReuseExecutor中通过statementMap字段缓存使用过的Statement对象,key是SQL语句,value是SQL对应的Statement对象。ReuseExecutor.doQuery()、doQueryCursor()、doUpdate()方法的实现与SimpleExecutor对应方法的实现一样,区别在于其中调用的preparestatement()方法,SimpleExecutor每次都会通过JDBC Connection创建新的Statement对象,而ReuseExecutor会先尝试重用StaternentMap缓存的Statement对象。
ReuseExecutor. prepareStatement()方法的具体实现如下:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 获取BoundSql对象
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 判断该SQL是否缓存过Statement
if (hasStatementFor(sql)) {
// 获取缓存的Statement对象
stmt = getStatement(sql);
// 修改超时时间
applyTransactionTimeout(stmt);
} else {
Connection connection = getConnection(statementLog);
// 创建新的Statement对象,并缓存到statementMap集合中
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
当事务提交或回滚、连接关闭时,都需要关闭这些缓存的Statement对象 。前面介绍了BaseExecutor.commit()、 rollback()和close()方法时提到,其中都会调用doFlushStatements()方法,所以在该方法实现关闭Statement对象的逻辑非常合适,具体实现如下:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) {
// 遍历statementMap集合并关闭其中的Statement对象
for (Statement stmt : statementMap.values()) {
closeStatement(stmt);
}
// 清空statementMap
statementMap.clear();
return Collections.emptyList();
}
这里需要注意一下ReuseExecutor.queryCursor()方法的使用,熟悉JDBC编程的读者知道,每个Statement对象只能对应一个结果集,当多次调用queryCursor()方法执行同SQL语句时,会复用Statement对象,只有最后一个ResultSet是可用的 。而queryCursor()方法返回的Cursor对象,在用户迭代Cursor对象时,才会真正遍历结果集对象并进行映射操作,这就可能导致使用前面Cursor对象中封装的结果集关闭。
5.BatchExecutor
应用系统在执行一条SQL语句时,会将SQL语句以及相关参数通过网络发送到数据库系统。对于频繁操作数据库的应用系统来说,如果执行SQL语句就向数据库发送一次请求,很多时间会浪费在网络通信上。使用批量处理的优化方式可以在客户端缓存多条SQL语句,并在合适的时机将多条SQL语句打包发送给数据库执行,从而减少网络方面的开销,提升系统的性能。
不过有一点需要注意,在批量执行多条SQL语句时,每次向数据库发送的SQL语句条数是有上限的,如果超过这个上限,数据库会拒绝执行这些SQL语句井抛出异常 所以批量发送SQL语句的时机很重要。
BatchExecutor实现了批处理多条SQL语句的功能,其中核心字段的含义如下:
// 缓存多个Statement对象其中每个Statement对象中都缓存了多条SQL语句
private final List<Statement> statementList = new ArrayList<>();
// 记录批处理的结果,BatchResult中通过updateCounts字段记录每个Statement执行批处理的结果
private final List<BatchResult> batchResultList = new ArrayList<>();
// 记录当前执行的SQL语句
private String currentSql;
// 记录当前执行的MappedStatement对象
private MappedStatement currentStatement;
JDBC中的批处理只支持insert、update、delete等类型的SQL语句,不支持select类型的SQL语句,所以下面要分析的是BatchExecutor.doUpdate方法。
BatchExecutor.doUpdate()方法在添加一条SQL语句时,首先会将currentSql字段记录的SQL语句以及currentStatement字段记录的MappedStatement对象与当前添加的SQL以及MappedStatement对象进行比较,如果相同则加到同一个Statement对象等待执行,如果不同则创建新的Statement对象并将其缓存到statementList集合中等待执行。doUpdate()方法具体实现如下:
@Override
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();
// 获取SQL语句
final String sql = boundSql.getSql();
final Statement stmt;
// 如果当前执行的SQL模式与上次执行的SQL模式相同且对应的MappedStatement对象相同
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 获取statementList中的最后一个
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
// 绑定实参
handler.parameterize(stmt);// fix Issues 322
// 查找对应的BatchResult对象,并记录用户传入的实参
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
// 创建新的Statement对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 绑定实参
handler.parameterize(stmt); // fix Issues 322
// 更新currentSql、currentStatement
currentSql = sql;
currentStatement = ms;
// 添加刚创建的Statement对象
statementList.add(stmt);
// 添加新的BatchResult对象
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 底层通过调用Statement.addBatch()方法添SQL语句
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
JDBC批处理功能中Statement可以添加不同模式的SQL,但是每添加一个新模式的SQL语句都会触发一次编译操作PreparedStatement中只能添加同一模式的SQL语句,只会触发一次编译操作,但是可以通过绑定多组不同的实参实现批处理。通过上面对doUpdate()方法的分析可知,BatchExecutor会将连续添加的、相同模式的SQL语句添加到同一个Statement/PreparedStatement对象中,这样可以有效地减少编译操作的次数。
在添加完待执行的SQL语句之后来看一下BatchExecutor.doFlushStatemnts()方法是如何批量处理这些SQL语句的:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
// 用于储存批量处理的结果
List<BatchResult> results = new ArrayList<>();
// 如果明确指定了要回滚事务,则直接返回空集合,忽略statementList集合中记录的SQL语句
if (isRollback) {
return Collections.emptyList();
}
// 遍历statementList集合
for (int i = 0, n = statementList.size(); i < n; i++) {
// 获取Statement对象
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
// /获取对应的BatchResult对象
BatchResult batchResult = batchResultList.get(i);
try {
// 调用Statement.executeBatch()方法批量执行其中记录的SQL语句,并使用返回的int数组
// 更新BatchResult.updateCounts字段,其中每一个元素都表示一条SQL语句影响的记录条数
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
// 获取配置的KeyGenerator对象
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
// 获取数据库生成的主键,并设置到parameterObjects中
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
// 对于其他类型的keyGenerator,会调用其processAfter()方法
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
// 关闭Statement对象
closeStatement(stmt);
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
// 添加BatchResult到results集合
results.add(batchResult);
}
return results;
} finally {
// 关闭Statement对象,并清空currentSql字段、清空statementList集合、清空batchResultList集合
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
BatchExecutor.doQuery和doQueryCursor()方法的实现与前面介绍的SimpleExecutor类似,主要区别就是BatchExecutor中的这两个方法在最开始都会先调用flushStatement()方法,执行缓存SQL语句,这样才能从数据库中查询到最新的数据。
6.CachingExecutor
CachingExecutor是Executor接口的装饰器,它为Executor对象增加了二级缓存的相关的功能,先来简单介绍MyBatis中的二级缓存及其依赖的相关组件。
二级缓存简介
MyBatis中提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。与二级缓存相关的配置有三个:
(1)首先是mybatis-config.xml配置文件中的cacheEnabled配置,它是二级缓存的总开关。只有当该配置设置为true时,后面两项的配置才会有效果,cacheEnabled的默认值为 true 。具体配置如下:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
(2)在前面介绍映射配置文件的解析流程时提到,映射配置文件中可以配置<cache>节点或<cache-ref>节点。如果映射配置文件中配置了这两者中的任一一个节点,则表示开启了二级缓存功能。如果配置了<cache>节点,在解析时会为该映射配置文件指定的命名空间创建相应的Cache对象作为其二级缓存,默认是PerpetualCache对象,用户可以通过<cache>节点的type属性指定自定义Cache对象。如果配置了<cache-ref>节点,在解析时则不会为当前映射配置文件指定的命名 空间创建独立的Cache对象,而是认为它与<cache-ref>节点的namespace属性指定的命名空间共享同一个Cache对象。通过<cache>节点和<cache-ref>节点的配置,用户可以在命名空间的粒度上管理二级缓存的开启和关闭。
(3)最后一个配置项是<select>节点中的useCache属性,该属性表示查询操作产生的结果对象是否要保存到二级缓存中。useCache属性的默认值是true。
TransactionalCache&TransactionalCacheManager
TransactionalCache和TransactionalCacheManager是CachingExecutor依赖的两个组件。其中,TransactionalCache继承了Cache接口,主要用于保存在某个SqlSession的某个事务中需要向某个二
级缓存中添加的缓存数据。TransactionalCache中核心字段的含义如下:
// 底层封装的二级缓存所对应的Cache对象
private final Cache delegate;
// 当该字段为true时,则表示当前TransactionalCache不可查询,且提交事务时会将底层Cache清空
private boolean clearOnCommit;
// 暂时记录添加到TransactionalCache中的数据。在事务提交时,会将其中的数据添加到二级缓存中
private final Map<Object, Object> entriesToAddOnCommit;
// 记录缓存未命中的CacheKey对象
private final Set<Object> entriesMissedInCache;
TransactionalCache.putObject()方法并没有直接将结果对象记录到其封装二级缓存中,而是暂时保存在 entriesToAddOnCommit集合中,在事务提交时才会将这些结果对象从entriesToAddOnCommit集合添加到二级缓存中。 putObject()方法的具体实现如下:
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
再来看TransactionalCache.getObject()方法,它首先会查询底层的二级缓存,并将未命中的key添加到entriesMissedInCache中,之后会根据clearOnCommit字段的值决定具体的返回值,具体实现如下:
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
// 将未命中的key添加到entriesMissedInCache中
entriesMissedInCache.add(key);
}
// issue #146
// 根据clearOnCommit字段的值决定具体的返回值
if (clearOnCommit) {
return null;
} else {
return object;
}
}
TransactionalCache.clear()方法会清空entriesToAddOnCommit集合,并设置 clearOnCommi为true 。
TransactionalCache. commit()方法会根据clearOnCommit字段的值决定是否清空二级缓存,然后调用 flushPendingEntries()方法将entriesToAddOnCommit集合中记录的结果对象保存到二级缓存中,具体实现如下:
public void commit() {
// 在事务提交前 清空二级缓存
if (clearOnCommit) {
delegate.clear();
}
// 将entriesToAddOnCommit集合中的数据保存到二级缓存
flushPendingEntries();
// 重置clearOnCommit为false ,并清空entriesToAddOnCommit和entriesMissedInCache 集合
reset();
}
private void flushPendingEntries() {
// 遍历entriesToAddOnCommit集合,将其中记录的缓存项添加到二级缓存中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 遍历entriesMissedInCache集合,将entriesToAddOnCommit集合中不包含的缓存项添加到二级缓存中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
TransactionalCache.rollback()方法会将entriesMissedlnCache集合中记录的缓存项从二级缓存中删除,并清空entriesToAddOnCommit集合和entriesMissedlnCache集合,具体实现如下:
public void rollback() {
// 将entriesMissedInCache集合中记录的缓存项从二级缓存中删除
unlockMissedEntries();
// 遍历entriesMissedInCache集合,将entriesToAddOnCommit集合中不包含的缓存项添加到二级缓存中
reset();
}
TransactionalCacheManager用于管理CachingExecutor使用的二级缓存对象,其中只定义了transactionalCaches字段,它的 key 是对应的CachingExecutor使用的 二级缓存对象,value是相应TransactionaCach对象,在该TransactionalCache中封装了对应二级缓存对象,也就是这里的 key。
TransactionalCacheManager的实现比较简单,下面简单介绍各个方法的功能和实现。
*clear()方法、 putObject方法、 getObject()方法:*调用指定二级缓存对应的TransactionalCache对象的对应方法,如果transactionalCaches集合中没有对应的TransactionalCache对象,则通过getTransactiona!Cache()方法创建。
private TransactionalCache getTransactionalCache(Cache cache) {
// 如果transactionalCaches集合中没有对应的TransactionalCache对象,则新建
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
*commit()方法、 rollback()方法:*遍历transactionalCaches集合,井调用其中各个TransactionalCache对象的相应方法。
CachingExecutor的实现
CachingExecutor.query()方法执行查询操作的步骤如下:
(1)获取BoundSql对象,创建查询语句对应的CacheKey对象。
(2)检测是否开启了二级缓存,如果没有开启二级缓存,则直接调用底层Executor对象的query()方法查询数据库。如果开启了二级缓存,则继续后面的步骤。
(3)检测查询操作是否包含输出类型的参数,如果是这种情况,则报错
(4)调用TransactionalCacheManager.getObject()方法查询二级缓存,如果二级缓存中查找到相应的结果对象,则直接将该结果对象返回。
(5)如果二级缓存没有相应的结果对象,则调用底层Executor对象的query()方法。正如前面介绍的 ,它会先查询一级缓存,一级缓存未命中时,才会查询数据库。最后还会将得到的结果对象放入TransactionalCache.entriesToAddOnCommit集合中保存。
CachingExecutor.query()方法的具体代码如下:
@Override
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);
}
@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) {
// 根据<select>节点的配置,决定是否需妥清空二级缓存
flushCacheIfRequired(ms);
// 检测SQL节点的useCache配置以及是否使用了resultHandler配置
if (ms.isUseCache() && resultHandler == null) {
// 二级缓存不能保存输出类型的参数 如果查询操作调用了包含输出参数的存储过程,则报错
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 查询二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 二级缓存没有相应的结果对象,调用封装的Executor对象的query()方法
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将查询结果保存到TransactionalCache.entriesToAddOnCommit集合中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没有启动二级缓存,调用封装的Executor对象的query()方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
再看CachingExecutor.commit()和rollback()方法的实现,它们首先调用底层的Executor对象的对应方法完成提交和回滚,然后调用TransactionalCacheManager的对应方法完成对二级缓存的操作。具体实现如下:
@Override
public void commit(boolean required) throws SQLException {
// 调用底层的Executor提交事务
delegate.commit(required);
// 遍历相关TransactionalCache对象执行commit()方法
tcm.commit();
}
@Override
public void rollback(boolean required) throws SQLException {
try {
// 调用底层的Executor回滚事务
delegate.rollback(required);
} finally {
if (required) {
// 遍历相关TransactionalCache对象执行rollback()方法
tcm.rollback();
}
}
}