这部分内容需要掌握mysql的sql执行流程和事务控制才能理解,可以参考下我的相关文章。

mybatis sql执行流程

mybatis事务控制

一、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执行流程和事务控制才能理解,可以参考下我的相关文章。

mybatis sql执行流程

mybatis事务控制