mybatis源码解析九(mybatis的Executor)

今天我们一起来分析下mybatis中的Executor,我们暂且翻译为执行器吧,在mybatis中有以下执行器
1:BaseExecutor;
2:CachingExecutor;
3:SimpleExecutor;
4:ReuseExecutor;
5:BatchExecutor;
BasExecutor就是个基类,是一个抽象类,提供了执行的增删改查,关闭资源等等一列类方法,真正做事情的都是各个实现类
CachingExecutor:就是增加了缓存的执行器,
SimpleExecutor:mybatis的默认执行器,就是相当于一套sql语句执行一次数据库交互操作
ReuseExecutor:重用执行器:在同一个sqlSession反的范围下,同一个sql语句会缓存起来,重复利用,
BatchExecutor:顾名思义,批量执行器,就是我们执行批量更新或者批量插入的时候可以使用这个执行器,单挑语句执行,最后批量提交

鉴于此,这本篇文章,我就只对批量的执行器执行下分析,其他就不做具体的分析了,大家伙有兴趣的就自行看看源码
因为可能我们在项目中,会经常遇到的场景就是会有批量新增,或者批量删除,批量更新,的场景,一般我们会怎嘛处理这些那,大家肯定会想到一种处理方式,那就是sql语句中利用<foreach>标签,这个我就不做案例了,网上案例太多,可能在数据量小的时候,我们这样处理,没有什么问题,但是当数据量大的时候,比如一次性插入上千条,上万条,等等,会有可能让数据库宕机,所以,鉴于此,希望大家尽量不要再项目中使用循环的批量插入,这次,我们使用一种新的模式,来处理批量的数据处理,这里我们就要借用到mybatis的Batch执行器了,
我会从两个方面来分析,第一种方案,使用原生的DefaultSqlSession来操作,第二种方案,使用spring提供的SqlsessionTemplete来操作
话不多说,我们通过案例,然后窥探下myabtis的源码是怎样实现批量的哈
我们先看一下SqlSessionFactory这个接口申明的方法

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

可以看到定义很多重载方法,一看明了,不用在多做介绍了,有获取连接的,定义事物的,定义执行器类型的等等
接下来,进入主题,我这边写了个demo,我们通过demo来分析

@RunWith(SpringRunner.class)
@SpringBootTest
public class SqlBatch {


    @Autowired
    private SqlSessionFactory sqlSessionFactory;



    @Test
    public void testBatch(){
                //通过sqlsessionFactory指定执行器的类型为BATCH,自动提交设置为false
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
                //获取mapper接口类
        RyxAccountMapper mapper = sqlSession.getMapper(RyxAccountMapper.class);
                //循环插入10条记录
        for (int i =0;i<10;i++){
            RyxAccount ryxAccount = new RyxAccount();
            ryxAccount.setName(i+"小五");
            ryxAccount.setMoney(new BigDecimal(3));
            mapper.insert(ryxAccount);
        }
                //执行提交
        sqlSession.commit();
                //关闭资源
        sqlSession.close();
    }
}

案例很简单,就个简单的向数据库插入10条记录,我们运行,然后跟着源码一起来看,mybatis到底是怎样执行批量插入的
当我们运行的时候,会先进入DefaultSqlSession类,这个是myabtis的默认处理sqlSession的类,所有的主要逻辑都在这个类中开始实现的,终于怎样通过mapper接口找到这个的,就是通过动态代理生成代理类,这个我已经分析过了,就不在分析了
我们运行下代码


image.png

获取代理类后,执行mapperProxy,将Mapper方法放入缓存,执行mapperMethod.execute,确定执行语句的种类


image.png

接下来,进入我们的DefaultSqlSession类,的update方法(insert也是定义为update执行方法)
 @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      //调用基类执行器
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

重点关注executor.update防方法,这里由于我们定义了执行器的类型,就不会再走默认的Simple了,而是会走Batch方法

  
@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.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

#BatchExecutor类
@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;
    //当第一次执行的时候,currentSql中还没有值,会进入if分支,当执行到第二次的时候,就会进入else分支
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
     handler.parameterize(stmt);//fix Issues 322
    //将当前的statement,sql,参数对象放入batchResultList集合中,如下图所示,这里有个问题,
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      //获取连接
      Connection connection = getConnection(ms.getStatementLog());
      //创建statement对象
      stmt = handler.prepare(connection, transaction.getTimeout());
       //设置映射参数
      handler.parameterize(stmt);    //fix Issues 32
      currentSql = sql;
      currentStatement = ms;
      //将statement对象放入集合
      statementList.add(stmt);
      //将当前的statement,sql,参数对象放入batchResultList集合中,如下图所示
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
  // handler.parameterize(stmt);
    //执行批量方法
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }
image.png
@Override
  public void batch(Statement statement) throws SQLException {
    delegate.batch(statement);
  }

 @Override
  public void batch(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //这里会根据我们使用的不同的连接池调用不同的方法,我目前使用的是阿里的druid连接池,所以会去调用
    //DruidPooledPreparedStatement(这块暂时没有看的特别明白,所以下面的分析可能就不是特别正确)
    //大概可能就是通过myabtis的拦截器,自定义druid的处理逻辑
    ps.addBatch();
  }

 @Override
    public void addBatch() throws SQLException {
        //检查statement对象
        checkOpen();
        try {
            //执行com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl方法
            stmt.addBatch();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

 @Override
    public void addBatch() throws SQLException {
        createChain().preparedStatement_addBatch(this);
    }

@Override
    public void preparedStatement_addBatch(PreparedStatementProxy statement) throws SQLException {
        if (this.pos < filterSize) {
            nextFilter().preparedStatement_addBatch(this, statement);
            return;
        }
        //将参数添加到statement对象中
        statement.getRawObject().addBatch();
    }

当我们处理完数据的时候,会有如下结果,paremetorObjects会存储我们需要新增的数据列表,


image.png

现在咱们数据已经构造完毕,sql语句也已经准备好,接下来,就是要提交逻辑了,咱们会调用sqlSession.commit

 @Override
  public void commit(boolean force) {
    try {
      //最终调用BatchExecutor的commit
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

 @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    //清空本地缓存
    clearLocalCache();
    //刷新对象方法
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }
最终的执行逻辑在doFlushStatements,继续往下看
  public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    return doFlushStatements(isRollBack);
  }

@Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<BatchResult>();
      if (isRollback) {
        return Collections.emptyList();
      }
      //简历statement集合
      for (int i = 0, n = statementList.size(); i < n; i++) {
        //获取statement对象
        Statement stmt = statementList.get(i);
        //设置事物超时时间
        applyTransactionTimeout(stmt);
        //获取batchResult对象
        BatchResult batchResult = batchResultList.get(i);
        try {
          //执行完这句代码后,数据库的数据就已经插入完成了,主要逻辑在stmt.executeBatch()中
          batchResult.setUpdateCounts(stmt.executeBatch());
          MappedStatement ms = batchResult.getMappedStatement();
          //获取参数对象集合,这个时候,还是没有主键的,以下代码主要是为参数对象填充主键
          //也就是说,这里有10个参数对象,就会循环10次,为每一个对象生成同一个主键,但是一直没有想到这样的设计目的
          //假如这里我们批量插入1000条数据的时候,这里就循环1000次,返回的对象不同,但是主键是一样的,
          最后循环调用keyGenerator.processAfter方法,完成主键的查询和赋值,这段代码就不具体分析了,
          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集合中,包含了,当前的sql,已经入库的有主键的参数对象,statement对象
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }

后面的逻辑那,提交完了,之后,就开始关闭连接,这里咱们用的是连接池,所以不是真的关闭连接,而是将连接的资源放回到连接池中,下次继续使用.
接下来,我们使用spring的方式看看myabtis的如何操作的批量sql插入,我也是写了个demo,咱们一起通过demo来分析源码,这里大家注意下,我这里加上了事物注解,如果没有事物注解,这里不是批量提交的,而是,插入一行,提交一行,千万注意,咱们在使用spring整合mybatis的时候,如果要使用mybatis的批量插入,在修改执行器的类型后,一定要加入事物注解否则,是没有意义的,达不到一行一行插入,批量提交的结果,而是会插入一行,提交一行,还有一点注意下,这里不需要我们手动提价,而是spring还自动帮助我们提交,如果我们手动调用提交方法,会直接报错的,然后其他就没有什么区别了,至于为什么会在事物的情况下,才有用,我前面在分析sqlSession线程安全的问题的时候,已经描述过了,如果没有事物注解,spring每次会重新new一个DefaultSqlSession,不管执行器是什么类型,再执行完相应的SQL语句后,spring会自动提交,源码里写的很清楚,我们先看看没有添加事物注解的情况下过程

    @Test
    public void testBatch(){
        sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
        final RyxAccountMapper mapper = sqlSessionTemplate.getMapper(RyxAccountMapper.class);
        for (int i =0;i<10;i++){
             RyxAccount ryxAccount = new RyxAccount();
             ryxAccount.setName(i+"小新");
            ryxAccount.setMoney(new BigDecimal(3));
            mapper.insert(ryxAccount);
        }
    }

demo很简单,就不做分析了,我们直接进入源码,重点地方我都会添加注释

private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //获取sqlSession
            SqlSession sqlSession = getSqlSession(
                    SqlSessionTemplate.this.sqlSessionFactory,
                    SqlSessionTemplate.this.executorType,
                    SqlSessionTemplate.this.exceptionTranslator);
            try {
                //执行invoke方法,调用DefaultSqlSession的相关逻辑
                Object result = method.invoke(sqlSession, args);
                                //判断当前的sqlSession是否是被事物管理,如果不是事物管理,直接提交,如果,是事物管理,则执行下一步,
                                //由于未被事物管理,就失去了批量提交的意义,这里会直接提交,
                if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    // force commit even on non-dirty sessions because some databases require
                    // a commit/rollback before calling close()
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                Throwable unwrapped = unwrapThrowable(t);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            } finally {
                                //这里关闭session也是一样的,如果存在事务的话,是将当前占用的资源的计数器减一,如果没有事物才是将            
                                //sqlSession关闭
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }
            }
        }
    }

后续的代码已经分析过了我就不做具体的分析了,从以上我们可以总结下
当使用sqlsessionFactry获取的sqlSession是DefaultSqlsession的时候,我们执行批量提交,需要手动关闭sqlSession
当使用SqlsessionTemplete的时候,如果我们没有添加事物,那样的结果就是,和普通的执行没有区别的,会一条条的执行,提交,因为sprsqlSesisonTemplete会判断是不是使用了,事物,如果没有使用事物,会自动提交当前的sqlSession会话
当开启事物的时候,才会执行批量的提交,这里有一点需要注意下,在测试的时候,需要将事物管理的方法写到service上,我用的springboot,在测试类中,直接添加事物注解,不会执行提交的,而是会直接回滚,

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容