Mybatis BatchExecutor源码分析

阿凡达2018-07-13 09:36

在介绍Mybatis批处理之前,首先回顾一下PreparedStatement的批处理:

假如现在需要向用户表中插入两条数据,如果不适用批处理的实现如下,有三次网络交互 。
 //加载驱动
    Class.forName("org.hsqldb.jdbcDriver");
    //获取连接
    Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:batch_keys", "sa", "");
    //设置不自动提交
    connection.setAutoCommit(false);
    //创建prepareStatement,存在一次网络交互,预存储statement
    PreparedStatement preparedStatement=connection.prepareStatement("insert into users values(?,?) ");
    //设置参数
    preparedStatement.setInt(1, 1);
    preparedStatement.setString(2, "Cujo");
    //执行sql 网络交互一次
    preparedStatement.executeUpdate();

    preparedStatement.setInt(1, 2);
    preparedStatement.setString(2, "zhangsan");
    preparedStatement.addBatch();
    //执行sql 网络交互一次
    preparedStatement.executeUpdate();
   为了减少网络交互次数可以使用批处理的方式实现,代码如下:
 //加载驱动
    Class.forName("org.hsqldb.jdbcDriver");
    //获取连接
    Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:batch_keys", "sa", "");
    //设置不自动提交
    connection.setAutoCommit(false);
    //创建prepareStatement,存在一次网络交互,预存储statement
    PreparedStatement preparedStatement=connection.prepareStatement("insert into users values(?,?) ");
    //设置参数
    preparedStatement.setInt(1, 1);
    preparedStatement.setString(2, "Cujo");
    //添加到批处理
    preparedStatement.addBatch();

    preparedStatement.setInt(1, 2);
    preparedStatement.setString(2, "zhangsan");
    preparedStatement.addBatch();
    //执行批处理,该处将添加到批处理中的sql都执行完
    preparedStatement.executeBatch();
 上文代码中可以看到可以有效减少网络交互次数,如果插入100条记录,就可以减少99次网络交互;这样效果就比较明显了。下面看一下Mybatis中的批处理是如何处理的。
主要的处理过程:
  1. 首先指定ExecutorType.BATCH打开sqlSession;
  2. 多长重复执行单挑SQL;
  3. 执行结束后调用flushStatements()执行批处理;
  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    //获取配置信息
    final Configuration configuration = ms.getConfiguration();
    //创建StatementHandler
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    //获取BoundSql
    final BoundSql boundSql = handler.getBoundSql();
    //从boundSql中获取 sql
    final String sql = boundSql.getSql();
    final Statement stmt;
    //如果sql等于currentSql同时MappedStatement与currentStatement相同, 就是同一条SQL,但是参数可能不同,这样就不需要重复创建PrepareStatement
    //可以减少网络交互次次数,通过源码可以发现批处理中最佳时间就是同样的sql要一起执行,不要存在不同sql间隔这样的场景出现
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      //获取最后一次创建statement
      stmt = statementList.get(last);
      //设置事务超时时间
      applyTransactionTimeout(stmt);
      //设置stmt参数
      handler.parameterize(stmt);
      //获取对应的批量结果
      BatchResult batchResult = batchResultList.get(last);
      //将参数对象添加到参数列表中
      batchResult.addParameterObject(parameterObject);
    } else {//和上一次创建的SQL不同,则需要重新创建PrepareStatement
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);  
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    //添加到批处理
    handler.batch(stmt);
    //返回默认值
    return BATCH_UPDATE_RETURN_VALUE;
  }
@Override
  public List doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List results = new ArrayList();
      if (isRollback) {
        return Collections.emptyList();
      }
      //遍历所有satement
      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执行批处理,并将更新条数保存到执行结果中;
          batchResult.setUpdateCounts(stmt.executeBatch());
          //获取结果对应到mappedStatement
          MappedStatement ms = batchResult.getMappedStatement();
          //获取参数列表
          List
  @Override
  public  List doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      //刷新
      flushStatements();
      //获取配置
      Configuration configuration = ms.getConfiguration();
      //创建StatementHandler,该处可以通过插件增强
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
      //获取连接
      Connection connection = getConnection(ms.getStatementLog());
      //预处理
      stmt = handler.prepare(connection, transaction.getTimeout());
      //设置参数
      handler.parameterize(stmt);
      //执行了SQL 也就是说查询是没有批处理的
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
   通过上文中的代码可以发现,在使用批处理的时候有几点需要注意:
  1. 在批处理过程中不要有查询SQL,如果有需要使用不同的executor;
  2. 在批量处理过程中,相同的SQL要一起处理,不要有不同SQL间隔添加到批处理中;
  3. 在大量的执行过程中可以部分执行flush,最后执行事务提交。

相关阅读:Mybatis延迟加载对象的序列化反序列化处理

Mybatis插件的使用以及源码分析

本文来自网易实践者社区,经作者张伟授权发布。