Note: This series of source code analysis is based on mybatis 3.5.6, the source code gitee warehouse warehouse address: funcy/mybatis .
mybatis
The operation of executing the sql statement is Executor
completed by the executor ( ), mybatis
which provides three types Executor
:
type | name | Function |
---|---|---|
REUSE |
reuse executor | Cached , the same can be reused PreparedStatement for the next executionsql |
BATCH |
batch executor | Record the modification operations locally, and wait for the program to trigger or perform the modification operations in batches when there is the next query |
SIMPLE |
simple executor | Generated for each execution PreparedStatement , closed after execution, not cached |
In addition, mybatis
a cache executor is also provided CachingExecutor
, which is actually a decoration class of the above three executors to handle cache-related operations. The actual work is still one of the above three executors.
Executor
The continuation structure is as follows:
1.BaseExecutor
BaseExecutor
Implemented Executor
basic operations, such as:
- Transaction processing:
commit(...)
: Handle the commit of the transactionrollback(...)
: Handles the rollback of the transaction
- Cache processing:
createCacheKey(...)
: Create cache keyclearLocalCache(...)
:clear cache
- curd operation:
query(...)
: query operationupdate(...)
: Update operations, insertions and deletions are also handled here
- left to the implementation of subclasses
doUpdate(...)
: The specific update operation is left to the subclass to implementdoQuery(...)
: The specific query operation is left to the subclass to implement
When we focus on implementation next Executor
, we only focus on methods that are left to subclasses to implement.
2.SimpleExecutor
SimpleExecutor
It will be generated for each execution PreparedStatement
, closed after execution, not cached, let's see how it is implemented, let's take a look at its doQuery(...)
methods:
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);
// 得到 PrepareStatement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询
return handler.query(stmt, resultHandler);
} finally {
// 关闭 Statement
closeStatement(stmt);
}
}
复制代码
Statement
The way to get it is SimpleExecutor#prepareStatement
:
private Statement prepareStatement(StatementHandler handler, Log statementLog)
throws SQLException {
Statement stmt;
// 获取数据库连接
Connection connection = getConnection(statementLog);
// 获取 Statement
stmt = handler.prepare(connection, transaction.getTimeout());
// 处理参数设置
handler.parameterize(stmt);
return stmt;
}
复制代码
This method first obtains the database connection, then obtains Statement
, and then processes the parameter settings.
关于数据库连接的获取,我们在分析配置文件的解析时,数据源的配置最终会转化成PooledDataSource
或UnpooledDataSource
对象,数据库连接就是从数据源来的。
至于Statement
的生成,PreparedStatement
的实例化操作方法为PreparedStatementHandler#instantiateStatement
,这些都是常规的jdbc操作,就不细看了。
处理sql的执行方法为PreparedStatementHandler#query
:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
复制代码
SimpleExecutor#doQuery(...)
的执行流程如下:
- 获取数据库连接
- 获取
PrepareStatement
- 执行查询
- 关闭
PrepareStatement
SimpleExecutor
的操作就是常规的jdbc操作。
3. ReuseExecutor
ReuseExecutor
会缓存PreparedStatement
,下一次执行相同的sql
可重用。
我们依然分析doQuery(...)
方法:
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.query(stmt, resultHandler);
}
复制代码
与SimpleExecutor
相比,ReuseExecutor
的doQuery(...)
方法并没关闭Statement
.我们来看看Statement
的获取操作:
private Statement prepareStatement(StatementHandler handler, Log statementLog)
throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 根据sql语句判断是否有Statement缓存
if (hasStatementFor(sql)) {
// 有缓存,直接使用
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
// 没缓存,获取数据库连接,再获取 Statement
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
// 缓存 Statement
putStatement(sql, stmt);
}
// 处理参数
handler.parameterize(stmt);
return stmt;
}
复制代码
可以看到,ReuseExecutor
获取Statement
时,会先从缓存里获取,缓存里没有才会新建一个Statement
,然后将新建的Statement
添加到缓存中。从这里可以看出,ReuseExecutor
的Reuse,复用的是Statement
。
我们再来看看缓存Statement
的结构:
public class ReuseExecutor extends BaseExecutor {
private final Map<String, Statement> statementMap = new HashMap<>();
...
private Statement getStatement(String s) {
return statementMap.get(s);
}
private void putStatement(String sql, Statement stmt) {
statementMap.put(sql, stmt);
}
}
复制代码
由些可见,缓存Statement
的是一个Map
,key
为sql
语句,value
为Statement
.
4. BatchExecutor
BatchExecutor
会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作,即:
- 进行修改操作(
insert
,update
,delete
)时,并不会立即执行,而是会缓存到本地 - 进行查询操作(
select
)时,会先处理缓存到本地的修改操作,再进行查询操作 - 也可行触发修改操作
从以上内容来看,这种方式似乎有大坑,列举几点如下:
- 修改操作缓存到本地后,如果执行前遇到意外重启,缓存的记录会不会丢失?
- 分布式环境下,多机共同协作,更新在A机上执行,查询在B机上执行,B机是不是不能查到B机的更新记录(B机的更新操作还在缓存中,并未执行)?
我们来看下BatchExecutor
的更新操作,进入doUpdate(...)
方法:
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)) {
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt);// fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
// 创建连接,获取 Statement
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); // fix Issues 322
currentSql = sql;
currentStatement = ms;
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 保存,等待之后批量执行
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
复制代码
BatchExecutor
有成员变量会记录上一次执行的sql
与MappedStatement
,如果本次执行的sql
与MappedStatement
与上一次执行的相同,则直接使用上一次的Statement
,否则就新建连接、获取Statement
.
得到Statement
后,会调用PreparedStatementHandler#batch
方法:
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.addBatch();
}
复制代码
这个方法并没有执行,只是调用PreparedStatement#addBatch
方法,将当前statement
保存了起来。
PreparedStatement#addBatch
方法如何使用呢?简单示意下:
// 获取连接
Connection connection = getConnection();
// 预编译sql
String sql = "xxx";
PreparedStatement statement = connection.prepareStatement(sql);
//记录1
statement.setInt(1, 1);
statement.setString(2, "one");
statement.addBatch();
//记录2
statement.setInt(1, 2);
statement.setString(2, "two");
statement.addBatch();
//记录3
statement.setInt(1, 3);
statement.setString(2, "three");
statement.addBatch();
//批量执行
int[] counts = statement.executeBatch();
// 关闭statment,关闭连接
...
复制代码
BatchExecutor
的doUpdate(...)
方法并没有执行sql语句,我们再来看看doQuery(...)
方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 处理缓存中的 statements
flushStatements();
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject,
rowBounds, resultHandler, boundSql);
// 获取连接,获取Statement,处理参数
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
// 执行查询
return handler.query(stmt, resultHandler);
} finally {
// 关闭 Statement
closeStatement(stmt);
}
}
复制代码
doQuery(...)
方法会先调用flushStatements()
方法,然后再处理查询操作,整个过程基本同SimpleExecutor
一致,即"获取数据库连接-获取Statement
-处理查询-关闭Statement
"等几步。我们重点来看flushStatements()
方法的流程.
flushStatements()
方法最终调用的是BatchExecutor#doFlushStatements
方法,代码如下:
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<>();
if (isRollback) {
return Collections.emptyList();
}
// 遍历的statementList,statementList就是缓存statement的结构
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
// 关键代码:stmt.executeBatch(),批量执行sql
batchResult.setUpdateCounts(stmt.executeBatch());
...
} catch (BatchUpdateException e) {
...
}
results.add(batchResult);
}
return results;
} finally {
...
}
}
复制代码
BatchExecutor#doFlushStatements
方法的关键代码就是batchResult.setUpdateCounts(stmt.executeBatch());
了 ,其中的stmt.executeBatch()
就是批量执行更新操作了。
From the above analysis, it can be seen that the BatchExecutor#doUpdate(...)
method does not execute the SQL statement, but only converts the SQL statement into Statement
and caches it. When the BatchExecutor#doQuery(...)
method is executed, the cached is executed first Statement
, and then the query operation is executed. Of course, the method can also be manually called BatchExecutor#flushStatements
to execute the cache Statement
.
5.CachingExecutor
CachingExecutor
Different from the above three kinds of executors, it is a decorative class that can get data from the cache, and the actual work is still one of the above three kinds of executors:
public class CachingExecutor implements Executor {
// 具体的执行器
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
...
}
复制代码
From the code point of view, it is Executor
a subclass of , which has a member variable delegate
whose type is Executor
passed in by the constructor. That is to say, at the time of creation CachingExecutor
, one of the above three executors will be passed in, and CachingExecutor
it will be saved to a member variable delegate
.
CachingExecutor
The query(...)
method is as follows:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建缓存key
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) {
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);
}
复制代码
From the code point of view, CachingExecutor
when processing a query, it will be obtained from the cache first, and when it does not exist in the cache, the query(xxx)
method of the specific executor will be executed.
The link to the original text of this article: my.oschina.net/funcy/blog/… , limited to the author's personal level, there are inevitably mistakes in the text, welcome to correct me! Originality is not easy. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.