前言
在上层接口SqlSession分析中(MyBatis原理——用户交互接口SqlSession
)可以看到,它实际上是委托Executor来执行sql的。所以本文来分析下MyBatis的这个组件。
首先看下Executor的继承体系:
左侧是标准的”接口-抽象类-具体类“三层结构,接口定义规范;抽象类实现公共逻辑,并提供抽象模板方法;具体子类实现模板方法,提供个性功能。这是框架中很常见的一种设计。
右侧的CachingExecutor是为了实现MyBatis的二级缓存而设计的。
下面就自上而下来看下执行器Executor的设计。
接口Executor
Executor定义了数据库的增删改查方法(增删改合并为update
)、事务相关的commit
、rollback
,还有和缓存相关的一些功能。
抽象类BaseExecutor
首先看下BaseExecutor具有哪些属性字段:
public abstract class BaseExecutor implements Executor {
// 事务
protected Transaction transaction;
// 装饰的执行器
protected Executor wrapper;
// 延迟加载的内容
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 一级缓存,内置于BaseExecutor
protected PerpetualCache localCache;
// 处理输出参数,存储过程相关,不考虑
protected PerpetualCache localOutputParameterCache;
// 祖传大仓库
protected Configuration configuration;
// 记录查询深度,比如嵌套查询
protected int queryStack;
private boolean closed;
总结几点:
- 由于Executor定义了事务相关的方法,所以这里持有Transaction。
- 有个Executor类型的包装对象,因为二级缓存采用了装饰器模式,这是供二级缓存用的,不涉及二级缓存的话,一般该字段就是自身。
- 有个延迟加载的队列,用于嵌套查询的时候,保存外层查询结果,也有点缓存的意思。
- 一级缓存PerpetualCache,其实就是HashMap的简单再包装,仅仅只是多了个String类型的
id
,相当于是给缓存定义一个名字。
BaseExecutor主要就是实现了一级缓存,然后把具体增删改查方法再次下放。比如针对查询方法query
的实现:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取对应的sql
BoundSql boundSql = ms.getBoundSql(parameter);
// 缓存的key,另说。
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 {
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// 开始一个查询,查询栈深度要+1,这里栈深度表示嵌套查询的深度,即association和collection标签。sql语句上的嵌套是直接查的
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();
}
// 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;
// 先放一个占位符,占位符用于DeferredLoad#canLoad()方法,区分isCached()方法。可以先不考虑
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;
}
这里可能延迟加载相关的占位符EXECUTION_PLACEHOLDER
,DeferredLoad 类比较迷惑,可以暂时忽略。若再不考虑存储过程相关内容,那么BaseExecutor执行流程可以简单总结为:
- 先查一级缓存PerpetualCache(HashMap再包装),有则返回,没有则步骤2。
- 一级缓存放个占位符,然后去查询数据库,这里提供抽象模板方法
doQuery
。 - 查完后,将查询结果放入一级缓存,并返回。
针对增删改的update
方法更简单,因为只需要清空一级缓存即可:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
// 省略无关信息
// ...
clearLocalCache();
return doUpdate(ms, parameter);
}
BaseExecutor提供了四个模板方法:
// 增删改
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
// 刷新Statement,在commit提交的时候会调用
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
// 查询
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
// 游标相关
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
所以到BaseExecutor还并没有真正查询数据库,具体实现是在子类中。如开头的继承体系所示,其一共有3个子类,下面逐个分析:
具体子类
SimpleExecutor
最简单,也最常用的应该就是SimpleExecutor,来看下它所实现的增删改查模板方法:
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
@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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
// 使用完关闭Statement,这也就是SimpleExecutor和ReuseExecutor的区别
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 通过StatementHandler创建Statement
stmt = handler.prepare(connection, transaction.getTimeout());
// 填充参数
handler.parameterize(stmt);
return stmt;
}
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
// 动态代理包装,实现打印日志功能
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
可以看到两个模板方法实现几乎一致,总结一下步骤:
- 从仓库Configuration中新建StatementHandler对象;
- 从Transaction中获取数据库连接,这里就是JDBC的Connection了,不过这里有个小插曲就是可以根据配置,利用动态代理给Connection做一个小包装,实现打印sql语句的功能;
- 通过StatementHandler新建一个JDBC的Statement,并进行参数填充;
- 最后还是调用StatementHandler的对应方法,实现了数据库查询。
可以看到SimpleExecutor更多是在调度,自身最终也没有执行sql,而是进一步交给StatementHandler去执行。不过到此,已经看到了JDBC的东西了,StatementHandler就是通过操作JDBC的Statement来完成数据库查询的。
ReuseExecutor
ReuseExecutor其实和SimpleExecutor类似,只是正如其名,它对JDBC的Statement做了一个简单的缓存:用HashMap。而SimpleExecutor是每次都新建,用完就关闭。看ReuseExecutor#prepareStatement
方法的源码,注意和上边SimpleExecutor#prepareStatement
的区别
private final Map<String, Statement> statementMap = new HashMap<>();
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 缓存中有,则取出使用
if (hasStatementFor(sql)) {
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
// 缓存中没有再新建,并放入缓存
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
// 填充参数
handler.parameterize(stmt);
return stmt;
}
// 简单地去缓存中查询有没有这个key对应的Statement
private boolean hasStatementFor(String sql) {
try {
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);
}
BatchExecutor
在说BatchExecutor之前,先介绍点JDBC的相关知识。JDBC的Statement接口有个addBatch
方法,维护一个命令列表,用户可以向其中添加命令,然后再调用executeBatch
批量执行。比如:
PreparedStatement statement = connection.prepareStatement("INSERT INTO `student` VALUES(?, ?)");
// 第1条sql
statement.setInt(1, 1);
statement.setString(2, "zhangsan");
statement.addBatch();
// 第2条sql
statement.setInt(1, 2);
statement.setString(2, "lisi");
statement.addBatch();
// 第3条sql
statement.setInt(1, 3);
statement.setString(2, "wangwu");
statement.addBatch();
// 批量执行
int [] counts = statement.executeBatch();
connection.commit();
也就是攒一波sql再交给数据库执行,提高效率。
BatchExecutor正是利用这一点,针对同一Mapper执行的增删改做了优化。首先看下BatchExecutor的属性字段:
public class BatchExecutor extends BaseExecutor {
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
// 保存Statement列表
private final List<Statement> statementList = new ArrayList<>();
// 保存每个Statement对应的执行信息
private final List<BatchResult> batchResultList = new ArrayList<>();
// 下边两个字段类似栈顶指针,标记了列表中最后一个sql的信息
// 当前sql
private String currentSql;
// 当前MappedStatement
private MappedStatement currentStatement;
在增删改的模板方法doUpdate
中,BatchExecutor做了自己的个性化实现:
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
// 获取各种信息
final Configuration configuration = ms.getConfiguration();
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)
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 取出上次sql的Statement
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 {
// 若不等于上次执行的sql,则还是按照传统方式新建Statement
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); //fix Issues 322
// 标记当前执行的sql信息
currentSql = sql;
currentStatement = ms;
// 添加到列表中
statementList.add(stmt);
// 添加BatchResult
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 注意这里不再是handler.update,而是batch方法
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
和SimpleExecutor的主要区别就是最终调用了handler.batch
而不是handler.update
,将sql缓存在Statement中,而且这里还对Statement做了缓存,如果连续两次都是一样的sql(即sql.equals(currentSql) && ms.equals(currentStatement)
,注意这里的sql可以是带?
占位符的,并不是说sql完全相同)那么就取出上次的Statement用(参考上边的例子)。
这里提出一点个人的看法:这里的实现是仅对比上次的sql,那么如果多个sql交叉提交,就没法重用Statement了(每次sql都不一样)。如果保存为map映射,key为sql+MappedStatement,value是对应的Statement感觉更好,不过一般情况下同一sql总是在一块执行,这样也行,实现更简单。
但是这样并没有实际执行sql,BatchExecutor将执行放在了doFlushStatements
模板方法中。该模板方法在父类BaseExecutor的commit
中会被调用。下面是方法实现:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<>();
if (isRollback) {
return Collections.emptyList();
}
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
// 这里调用executeBatch批量执行了保存的sql
batchResult.setUpdateCounts(stmt.executeBatch());
// 省略绑定主键处理
// ...
closeStatement(stmt);
} catch (BatchUpdateException e) {//省略异常处理}
results.add(batchResult);
}
return results;
} finally {
// 关闭所有Statement 并清空缓存信息
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
主要做的事就是:遍历Statement列表,调用executeBatch
批量提交之前保存的sql给数据库执行;绑定主键信息(省略);关闭Statement;清空缓存列表信息。
Executor的创建
Executor由Configuration创建,方法很简单:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 装载插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
关于CachingExecutor的内容,放到缓存中另说。