这部分内容需要掌握mysql的sql执行流程和事务控制才能理解,可以参考下我的相关文章。
一、BatchExecutor的介绍
BatchExecutor 是mybatis提供的一个执行器, 用于执行批量更新操作,性能上比使用foreach标签拼sql要高,使用方式上也更方便。BatchExecutor 只是对更新操作做了增强,查询操作和普通的执行器是一样的。
二、BatchExecutor的使用
使用BatchExecutor时dao方法对应的sql是每次更新一条数据的,在java中循环数据集合攒sql,最后在使用jdbc的batch操作把sql发送到数据库执行
注意下边update方法批量执行sql的前提是要开启事务,原因需要在分析BatchExecutor的源码时解答
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Transactional
public void update() {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
Account account1 = new Account();
account1.setId("zs");
account1.setMoney(100);
Account account2 = new Account();
account2.setId("ls");
account1.setMoney(200);
List<Account> accountList = new ArrayList<>();
accountList.add(account1);
accountList.add(account2);
//通过sqlsession获取mapper接口的代理对象
IAccountDao accountMapper = sqlSessionTemplate.getMapper(IAccountDao.class);
for (Account account : accountList) {
//这个update方法每执行一次就会攒一个sql
//注意只有当前service方法开启事务的情况下才会有这种攒sql的效果,
//如果没有事务,这个update方法每次执行都是一个新的事务,新的数据库连接,sql执行完就会提交事务,不会攒sql,还是在单条执行
accountMapper.update(account);
}
//这句就是把攒的sql发到数据库执行
sqlSessionTemplate.flushStatements();
}
三、BatchExecutor源码分析
BatchExecutor的关键在doUpdate方法
public class BatchExecutor extends BaseExecutor {
@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;
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 {
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方法是关键
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
//调用sqlsession.flushStatements方法时最终会调用到这个方法,把攒的sql刷到数据库
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<BatchResult>();
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 {
//这个stmt.executeBatch()方法就是把sql批量刷到数据库,这是jdbc提供的方法
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
} 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);
}
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
}
在来分析下handler的batch方法,我们以PreparedStatementHandler
为例
//部分源码
public class PreparedStatementHandler extends BaseStatementHandler {
@Override
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//这里就是调用了jdbc的addBatch方法来攒sql
ps.addBatch();
}
}
上边的源码总结下来就是使用 statement.addBatch()
攒sql,使用stmt.executeBatch()
执行攒的sql,
至于为什么必须要求service方法开启事务呢,因为jdbc批量执行sql的原理就是要把多个sql攒到同一个数据库连接上然后一次执行,当不开启真个方法事务时,mapper方法每一次执行完都会提交事务,提交事务前就会把攒的sql刷到数据库,然后释放连接,下次mapper方法执行时又会重新获取数据库连接,重新开启事务。所以每一次都只是执行一个sql,没有批量的效果。
这部分内容需要掌握mysql的sql执行流程和事务控制才能理解,可以参考下我的相关文章。