JDBC方式
PreparedStatement中有两个方法addBatch()和executeBatch(),可以执行批量插入和更新,不能执行select查询
Connection connection = dataSource.getConnection();
long startTime = System.currentTimeMillis();
connection.setAutoCommit(false);
PreparedStatement preparedStatement = preparedStatement = connection.prepareStatement("insert into ry.sys_user (user_id,dept_id,login_name,user_name) values (?,?,?,?);");
for (int i = 1; i < 10000; i++) {
preparedStatement.setInt(1, i);
preparedStatement.setInt(2, i);
preparedStatement.setString(3, "悟空" + i);
preparedStatement.setString(4, "空空" + i);
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
connection.commit();
long eneTime = System.currentTimeMillis();
System.out.println(eneTime - startTime);
批量执行插入SQL语句:
begin;
update sys_user SET dept_id = 1, login_name = '空空1',user_name = '空空1', update_time = sysdate() where user_id = 1;
update sys_user SET dept_id = 2, login_name = '空空2',user_name = '空空2', update_time = sysdate() where user_id = 2;
update sys_user SET dept_id = 3, login_name = '空空3',user_name = '空空3', update_time = sysdate() where user_id = 3;
commit;
批量执行更新SQL语句:
insert into sys_user(user_id, dept_id, login_name, user_name, create_time)values(3, 3, '空空3', '空空3', sysdate()),(4, 4, '空空4', '空空4',sysdate()),(5, 5, '空空5', '空空5',sysdate());
无论是批量插入和更新都是一次预编译,一次提交。
Mybatis方式
在Mybatis中有三个执行器SimpleExecutor,ReuseExecutor,BatchExecutor。
query流程
Executor接口中query方法有两个重载,我们从参数少的这个开始入手,它内部也会调用到另外一个。默认情况下会使用CachingExecutor,所以我们从CachingExecutor#query()开始:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用重载query方法
return 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) {
// 刷新缓存
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;
}
}
//默认情况没有开启二级缓存,会直接走到这里
//delegate即BaseExecutor三个子类的其中一个
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码
进入CachingExecutor#query,首先通过MappedStatement获取BoundSql,创建缓存key,然后调用了重载的query方法。重载query查询在不考虑缓存的情况下,会直接通过委托执行器的query方法进行查询。 这里的委托执行器为BaseExecutor的子类,而BaseExecutor实现了query方法,所以我们先进入BaseExecutor#query()(同样先忽略一级缓存部分的逻辑):
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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
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;
//缓存占位
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;
}
复制代码
两个方法比较长,大部分是缓存处理。忽略缓存的情况下(直接看我注释的部分),从query方法调用了数据库查询方法queryFromDatabase,但是,真正查询的逻辑是在抽象方法doQuery中实现的,doQuery由BaseExecutor子类实现。我们依次看下子类实现逻辑:
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
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//准备Statement
stmt = prepareStatement(handler, ms.getStatementLog());
//执行查询
return handler.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;
}
复制代码
SimpleExecutor#doQuery方法代码比较简单,过程清晰明了,简单说明一下:
- 从MappedStatement对象获取全局Configuration配置对象;
- 调用Configuration#newStatementHandler创建StatementHandler对象;
- 创建并初始化Statement对象;
- 调用StatementHandler#query执行Statement,并使用resultHandler解析返回值;
- 最后关闭Statement。
从上述流程可知,doQuery方法调度StatementHandler完成了对Statement的初始化、参数设置、执行、结果处理与关闭,是对Statement整个生命周期的管理与控制,与前文所说的Executor参与了SQL语句执行的全过程名副其实。
ReuseExecutor#doQuery
//Statement缓存
private final Map<String, Statement> statementMap = new HashMap<>();
@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 stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
//检查缓存中是否存在当前sql
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;
}
复制代码
ReuseExecutor#doQuery与SimpleExecutor#doQuery的逻辑基本一致,不同点在于prepareStatement方法的实现逻辑。prepareStatement使用statementMap对执行过的sql进行缓存,只有statementMap中不存在当前sql的时候才会执行创建流程,对性能有一定的提升。需要注意的是,Executor对象是SqlSession的组成部分,所以这个缓存与SqlSession的生命周期一致。
BatchExecutor#doQuery
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
//批量执行,目前我理解是为了把之前批量更新的语句执行掉
flushStatements();
//获取Configuration对象
Configuration configuration = ms.getConfiguration();
//创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
//创建Statement
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
//设置Statement参数
handler.parameterize(stmt);
//执行并返回结果
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
复制代码
BatchExecutor#doQuery方法除了多执行flushStatements方法外,与SimpleExecutor基本一致,不再展开。
update()流程
update()方法的对应了insert、update、delete等不同的命令,其执行流程与query()方法流程类似。同样是经过CachingExecutor->BaseExecutor->SimpleExecutor/ReuseExecutor/BatchExecutor。把整个过程的代码一起贴出来分析(注释):
//org.apache.ibatis.executor.CachingExecutor#update
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 刷新缓存:开启缓存时,update命令默认情况是需要刷新缓存的
flushCacheIfRequired(ms);
//调用委托执行器进行update
return delegate.update(ms, parameterObject);
}
//org.apache.ibatis.executor.BaseExecutor#update
@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.");
}
// 清空缓存,然后调用子类的doUpdate方法
clearLocalCache();
//调用抽象方法执行数据库更新操作
return doUpdate(ms, parameter);
}
复制代码
Mybatis的缓存机制仅对查询有效,所以Executor能做的就是:使缓存失效、请求中转,最终调用doUpdate执行数据库操作,所以:
- CachingExecutor#update首先使二级缓存失效,然后调用委托执行器执行update操作。
- BaseExecutor#update也是首先使得一级缓存失效,然后调用抽象方法doUpdate执行数据库的更新操作。
SimpleExecutor#doUpdate
SimpleExecutor#doUpdate与doQuery完全一致,不再说明了。
ReuseExecutor#doUpdate
ReuseExecutor#doUpdate与doQuery完全一致,不再说明了。
BatchExecutor#doUpdate
BatchExecutor的执行与前两个不一样,它用于执行批量的sql命令,所以多了一些批量准备工作。为了减少与数据库的交互次数,BatchExecutor会批量执行sql命令。代码如下:
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
//获取Configuration对象
final Configuration configuration = ms.getConfiguration();
//创建StatementHandler
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
//获取sql对象
final BoundSql boundSql = handler.getBoundSql();
//获取sql与酒
final String sql = boundSql.getSql();
final Statement stmt;
//如果当前命令与上一次执行一样,就不再重复创建Statement,性能提升
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
//取出最后一条的索引
int last = statementList.size() - 1;
//取出最后一个Statement对象
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
//设置Statement参数
handler.parameterize(stmt);//fix Issues 322
//获取BatchResult
BatchResult batchResult = batchResultList.get(last);
//设置BatchResult参数对象
batchResult.addParameterObject(parameterObject);
} else {
//如果当前命令与上一次执行不一样,重新创建
Connection connection = getConnection(ms.getStatementLog());
//初始化、准备Statement
stmt = handler.prepare(connection, transaction.getTimeout());
//设置参数
handler.parameterize(stmt); //fix Issues 322
//设置当前执行的sql命令信息
currentSql = sql;
currentStatement = ms;
//存起来
statementList.add(stmt);
//保存结果对象
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
//批量处理
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
BatchExecutor#doUpdate方法完成了Statement执行前的准备工作,在准备Statement时与上一次要执行的Statement进行对比,如果一致则不再执行重新创建Statement的流程。所以,使用BatchExecutor时应该尽量执行相同的sql命令。
但是,BatchExecutor#doUpdate并未进行数据库的执行操作,它需要通过SqlSession#flushStatements进行触发,然后调用到BatchExecutor#doFlushStatements执行最终的操作,这里就不再展开了。