MyBatis Executor:SQL 执行的核心引擎
上一篇文章我们介绍了Mybatis是如何前置处理参数,把Mapper中可能的各种各样的参数,统一处理成Map,方便后续处理。接下来,继续深入,看下在SqlSession中,如何继续执行。
一、从SqlSession.selectOne说起
当我们通过SqlSession调用selectOne方法时,看似简单的操作背后,其实是Executor在完成实际工作。我们先从DefaultSqlSession的selectOne源码说起:
java
@Override
public <T> T selectOne(String statement, Object parameter) {
// 调用selectList获取结果集
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 but found: " + list.size());
} else {
return null;
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 获取MappedStatement(SQL元信息)
MappedStatement ms = configuration.getMappedStatement(statement);
// 核心:调用Executor的query方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database", e);
}
}
从源码可见,SqlSession的selectOne最终会委托给executor.query()方法。SqlSession更像是一个 "门面",而Executor才是真正负责SQL执行的核心组件。Executor接口定义了 MyBatis 执行 SQL 的标准,其实现类则根据不同场景提供了多样化的执行策略。
二、Executor 接口的实现体系
Executor是一个接口,定义了 SQL 执行的核心方法(query/update/commit/rollback等)。MyBatis 通过模板+策略模式,构建了灵活的执行器体系。下面是Executor相关的类图:
1. BaseExecutor:Executor的基类
BaseExecutor
是SimpleExecutor
、ReuseExecutor
等具体执行器的抽象父类,主要实现了Mybatis的一级缓存,也就是SqlSession级别的缓存。
一级缓存的实现说起来也简单,就是在查询的时候缓存,更新或者事务提交时清空缓存。
查询:
java
// BaseExecutor.java
@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);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 。。。省略大量代码
// 检查一级缓存
List<E> 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);
}
// ...省略大量代码
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 {
//调用子类必须实现的doQuery执行真正的查询。
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;
}
更新 & 提交
java
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//一旦更新,先清除缓存
clearLocalCache();
//调用子类需要实现的doUpdate方法
return doUpdate(ms, parameter);
}
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 提交前,清除缓存
clearLocalCache();
// 刷新statement,子类实现,BatchExecutor需要
flushStatements();
if (required) {
transaction.commit();
}
}
BaseExecutor是一个典型的模板模式,为子类提供了一级缓存的能力,子类只要继承它,并实现对应的方法,就可以实现缓存的能力。需要实现的方法有:
抽象方法 | 作用 | 子类实现差异 |
---|---|---|
doQuery |
执行查询操作(SELECT) | 不同执行器创建和管理 Statement 的方式不同 |
doUpdate |
执行更新操作(INSERT/UPDATE/DELETE) | 批量执行器会攒批处理,而非立即执行 |
doFlushStatements |
刷新statement(如批量操作的提交) | BatchExecutor 对更新批量提交;ReuseExecutor 批量关闭statement;SimpleExecutor 返回空列表 |
doQueryCursor |
执行游标查询(返回 Cursor ) |
与 doQuery 类似,但返回游标用于流式处理 |
下面我们看看各个子类的功能以及如何实现的。
2. SimpleExecutor:简单执行器(默认实现)
SimpleExecutor是 MyBatis 的默认执行器,每次执行SQL都会创建新的Statement对象,执行完毕后立即关闭。
以doQuery
为例,介绍SimpleExecutor
的实现:
java
@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.<E>query(stmt, resultHandler);
} finally {
// Statement对象使用后立即关闭
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
适用场景:大多数常规业务场景,尤其是SQL语句不重复、执行频率低的情况。由于无需维护Statement缓存,逻辑简单且稳定。
3. ReuseExecutor:可重用执行器
ReuseExecutor的核心是重用Statement对象 ,通过SQL语句作为key缓存Statement,避免重复创建。而Statement的关闭,则是在doFlushStatements
时执行,也就是,在事务提交、回滚或者Executor关闭的时候,才对所有缓存的Statement
进行关闭。
以doQuery
为例,介绍ReuseExecutor
的实现:
java
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 注意这里使用完Statement后,并没有立即关闭
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
}
// Statement缓存,key是SQL,value是Statement
private final Map<String, Statement> statementMap = new HashMap<String, Statement>();
/**
* 先尝试从statementMap获取Statement,没有才创建,并放到statementMap中
*/
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;
}
/**
* 批量关闭Statement,并清空缓存。
*/
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
for (Statement stmt : statementMap.values()) {
closeStatement(stmt);
}
statementMap.clear();
return Collections.emptyList();
}
小结一下,与SimpleExecutor
最大的不同是
- 缓存
Statement
,SQL完全相同,则直接复用,无需多次创建。 - 执行完成后没有立即关闭
Statement
,而是在事务提交、回滚或者Executor
关闭时,统一关闭。
适用场景:需要频繁执行相同SQL的场景(如循环查询相同结构的语句),可减少Statement创建的性能开销。
4. BatchExecutor:批量执行器
BatchExecutor
专为批量更新操作(INSERT/UPDATE/DELETE)设计,能将多个 SQL 攒批后统一提交,减少数据库交互次数。
BatchExecutor
需要重点关注doUpdate
方法。
java
private final List<Statement> statementList = new ArrayList<>();
private final List<BatchResult> batchResults = new ArrayList<>();
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 若SQL相同且Statement未关闭,复用Statement添加批处理
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt); // 设置参数
BatchResult batchResult = batchResults.get(last);
batchResult.addParameterObject(parameter);
} else {
// 新SQL,创建新Statement
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); // 设置参数
currentSql = sql;
currentStatement = ms;
statementList.add(stmt);
batchResults.add(new BatchResult(ms.getId(), sql, parameter));
}
handler.batch(stmt); // 添加到批处理
return BATCH_UPDATE_RETURN_VALUE;
}
// 提交批处理
@Override
public List <BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<>();
if (isRollback) {
return Collections.emptyList();
}
// 执行所有批处理语句
for (Statement stmt : statementList) {
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResults.get(statementList.indexOf(stmt));
try {
//调用Statement的executeBatch实现批量
batchResult.setUpdateCounts(stmt.executeBatch());
// 。。。
} catch (BatchUpdateException e) {
// 处理异常
}
results.add(batchResult);
}
return results;
} finally {
// 清空缓存
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResults.clear();
}
}
另外,在doQuery
和doQueryCursor
方法中,也会先执行下flushStatements
,以确保能查出最新的数据。
适用场景:批量导入、批量更新等场景,能显著减少数据库交互次数(如一次提交 1000 条插入语句)。
三、默认 Executor 与配置方式
1. 默认 Executor
MyBatis 的默认 Executor 是SimpleExecutor,这由Configuration类的默认配置指定:
ini
// Configuration.java
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
2. 更换默认 Executor
(1)全局配置(mybatis-config.xml)
通过settings标签配置默认执行器类型:
xml
<configuration>
<settings>
<!-- 可选值:SIMPLE、REUSE、BATCH -->
<setting name="defaultExecutorType" value="REUSE"/>
</settings>
</configuration>
(2)创建 SqlSession 时指定
通过SqlSessionFactory.openSession(ExecutorType)动态指定:
java
// 使用BATCH执行器
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
3. BatchExecutor 使用示例
批量插入用户的完整示例:
java
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 批量添加1000条记录(仅入队,不立即执行)
for (int i = 0; i < 1000; i++) {
User user = new User();
user.setName("User" + i);
user.setAge(20 + i % 30);
mapper.insert(user);
}
// 提交事务,执行批量插入
session.commit();
} catch (Exception e) {
// 异常回滚
session.rollback();
}
注意:BatchExecutor仅对insert/update/delete有效,查询操作仍会立即执行。
四、小结
Executor是MyBatis SQL 执行的核心引擎,其设计体现了多种设计模式的灵活运用:
- 模板方法模式:BaseExecutor定义执行流程,子类实现具体细节;
- 策略模式:不同执行器对应不同 SQL 执行策略,按需选择。
另外,Executor还有一个实现类,CachingExecutor,采用装饰器模式,用于实现Mybatis的二级缓存,这个我们下期再讲。