ibatis批量執行分析

最近作pos數據文件解析及入庫的開發,其中pos的流水文件一個文件中就包含8000多條數據,每次插入數據庫執行的sql都是相同的。所以考慮到使用批量插入來提高效率。查看ibatis的文檔,看到提供了startBatch和executBatch兩個方法,看名字大概就知道這兩個方法和批量執行有關。我立馬在以前的for循環外面加上了這兩個方法:spring

sqlMap.getSqlMapClient().startBatch();
for (Map <String, Object > map  : list) {
    sqlMap.insert(sqlId, map);
}
sqlMap.getSqlMapClient().executeBatch();
執行以後發現,效率跟沒加同樣,毫無提高。到網上查閱資料說是要在這外邊再加一層事務處理代碼才行。
或者是使用spring提供的SqlMapClientTemplate中的方法來執行:
sqlMap.execute( new SqlMapClientCallback() {
    @Override
    public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
       
        int len = list.size();
        executor.startBatch();
       
        for( int i = 0; i < len; i ++){
            executor.insert(test, list.get(i));
            if(i != 0 && i % 500 == 0){
                executor.executeBatch();
                executor.startBatch();
            }
        }
       
        executor.executeBatch();
        return null;
    }
});
 
針對ibatis的批量操做必須加事務的作法感到有點奇怪(雖然JDBC批量操做通常會加事務,但不是必須加)。因而大體的研究了一下ibatis批量操做有關的代碼。
涉及到的類大概有以下幾個:
SqlMapClientImpl :SqlMapClient的實現類
SqlMapSessionImpl :SqlMapSession的實現類
SqlMapExecutorDelegate :代理SqlMapSession中的數據庫操做
MappedStatement :映射的sql語句配置對象
SqlExecutor:真正執行sql語句
SessionScope :SqlMapSession中相關的設置信息
StatementScope :Statemente中相關的設置信息
 
須要明確的是SqlMapClient中的大部分方法其實是調用了SqlMapSession和SqlMapExecutorDelegate的方法,而SqlMapSession中的一些方法實際仍是調用了SqlMapExecutorDelegate的方法,也就是說大部分咱們操做的方法都實際有delegate來完成的。
直接與咱們打交道的是SqlMapClient,所以首先來看SqlMapClientImpl的startBatch方法:
public void startBatch() throws SQLException {
    getLocalSqlMapSession().startBatch();
}
public int executeBatch() throws SQLException {
    return getLocalSqlMapSession().executeBatch();
}
protected SqlMapSessionImpl getLocalSqlMapSession() {
    SqlMapSessionImpl sqlMapSession = (SqlMapSessionImpl) this.localSqlMapSession.get();
    if ((sqlMapSession == null) || (sqlMapSession.isClosed())) {
      sqlMapSession = new SqlMapSessionImpl( this);
      this.localSqlMapSession.set(sqlMapSession);
    }
    return sqlMapSession;
}
能夠看到這兩個方法實際上都委派給了SqlMapSession來操做的,那麼接下來的線索就在SqlMapSessionImpl類中了:
public void startBatch() throws SQLException {
    this.delegate.startBatch( this.sessionScope);
}
public int executeBatch() throws SQLException {
    return this.delegate.executeBatch( this.sessionScope);
}
這裏又能夠看到這兩個操做又委託給了delegate(SqlMapExecutorDelegate)來執行,最重要的操做都在delegate中了,這裏還涉及到了SessionScope,它用於傳遞一些配置信息。
public void startBatch(SessionScope sessionScope)
{
    sessionScope.setInBatch(true);
}
public int executeBatch(SessionScope sessionScope)
throws SQLException
{
    sessionScope.setInBatch(false);
    return this.sqlExecutor.executeBatch(sessionScope);
}
startBatch方法僅僅修改了SessionScope中的inBatch屬性的值,executeBatch也會重置SessionScope中inBatch的值,而且調用sqlExecutor的executeBatche方法來執行批量操做,注意這裏傳遞的參數只有sessionScope,那麼也就說明全部批量操做有關的信息都放置在SessionScope中。這裏咱們先來看sqlExecutor中是怎麼執行這些批量操做的。
public int executeBatch(SessionScope sessionScope)
    throws SQLException
{
    int rows = 0;
    Batch batch = (Batch)sessionScope.getBatch();
    if (batch != null) {
       try {
        rows = batch.executeBatch();
      } finally {
        batch.cleanupBatch(sessionScope);
      }
    }
    return rows;
}
這裏能夠看到,批量操做的信息是存放在sessionScope的batch屬性(Object類型)中的。是一個Batch類型的變量,獲取到以後直接調用Batch的executeBatch方法來完成批量操做的執行,而後再作一些清理操做。
public int executeBatch()
      throws SQLException
{
  int totalRowCount = 0;
  int i = 0; for ( int n = this.statementList.size(); i < n; ++i) {
    PreparedStatement ps = (PreparedStatement) this.statementList.get(i);
    int[] rowCounts = ps.executeBatch();
    for ( int j = 0; j < rowCounts.length; ++j) {
       if (rowCounts[j] == - 2)
        continue;
       if (rowCounts[j] == - 3) {
        throw new SQLException( "The batched statement at index " + j + " failed to execute.");
      }
      totalRowCount += rowCounts[j];
    }
  }
  return totalRowCount;
}
這裏就能夠看到底層實際仍是使用了JDBC的PreparedStatement來實現批量操做。這裏有個statementList對象,裏面存放了一些PreparedStatement,一個PreparedStatement對象就對應了同一個sql的批量操做處理。也就是說ibatis的批量操做中還支持多個不一樣sql的批量操做。
 
到這裏,批量操做的執行過程基本就分析完了,接下來就要分析,怎麼將這些PreparedStatement對象放置到Batch的statementList中去的。
 
經過文章最前面的代碼,能夠看到在startBatch和executeBatch以前執行的所有是insert操做,那麼PreparedStatement對象確定就是在insert方法執行的過程當中放置到Batch對象中去的。雖然調用的SqlMapClient的insert,但實際上會執行delegate的insert方法,所以咱們直接看SqlMapExecutorDelegate的insert方法:
public Object insert(SessionScope sessionScope, String id, Object param)
    throws SQLException
{
    Object generatedKey = null;

    MappedStatement ms = getMappedStatement(id); //獲取映射的sql配置信息
    Transaction trans = getTransaction(sessionScope); //獲取當前session的事務(SessionScope在一個session中惟一)
    boolean autoStart = trans == null; //判斷事務是否爲空
    try
    {
      trans = autoStartTransaction(sessionScope, autoStart, trans); //爲空則使用自動事務

      SelectKeyStatement selectKeyStatement = null;
      if (ms instanceof InsertStatement) {
        selectKeyStatement = ((InsertStatement)ms).getSelectKeyStatement();
      }

      Object oldKeyValue = null;
      String keyProperty = null;
      boolean resetKeyValueOnFailure = false;
      if ((selectKeyStatement != null) && ( !(selectKeyStatement.isRunAfterSQL()))) {
        keyProperty = selectKeyStatement.getKeyProperty();
        oldKeyValue = PROBE.getObject(param, keyProperty);
        generatedKey = executeSelectKey(sessionScope, trans, ms, param);
        resetKeyValueOnFailure = true;
      }

      StatementScope statementScope = beginStatementScope(sessionScope, ms); //生成StatementScope信息,其中包含sessionScope對象
      try {
        ms.executeUpdate(statementScope, trans, param); //使用MappedStatement對象執行,批量操做處理在這裏實現
      }
      catch (SQLException e)
      {
        if (resetKeyValueOnFailure);
        throw e;
      } finally {
        endStatementScope(statementScope);
      }

      if ((selectKeyStatement != null) && (selectKeyStatement.isRunAfterSQL())) {
        generatedKey = executeSelectKey(sessionScope, trans, ms, param);
      }
       //注意這裏,至關關鍵。若是是使用自動事務,那麼會自動提交事務
      autoCommitTransaction(sessionScope, autoStart);
    } finally {
      autoEndTransaction(sessionScope, autoStart);
    }

    return generatedKey;
}
這裏至關關鍵的地方就是自動事務的處理,批處理的和正常操做的區處理在MappedStatement中的executeUpdate方法中實現,這其中由調用了另一個名爲sqlExecuteUpdate的方法。
rows  = sqlExecuteUpdate(statementScope, trans.getConnection(), sqlString, parameters);
這個方法中判斷是執行批量操做仍是普通操做:
protected int sqlExecuteUpdate(StatementScope statementScope, Connection conn, String sqlString, Object[] parameters) throws SQLException {
    if (statementScope.getSession().isInBatch()) {
      getSqlExecutor().addBatch(statementScope, conn, sqlString, parameters);
      return 0;
    }
    return getSqlExecutor().executeUpdate(statementScope, conn, sqlString, parameters);
}
這裏就使用到了以前在startBatch中向SessionScope中設置的inBatch屬性,若是當前是在執行批處理操做,那麼就調用sqlExecutor的addBatch方法,若是是普通操做,那麼就調用sqlExecutor的executeUpdate來直接執行。
Batch是SqlExecutor的內部類,在SqlExecutor的addBatch中會從SessionScope中獲取batch屬性,若是爲空,則建立一個,而且設置到SessionScope中,而後在調用Batch對象的addBatch方法:
public void addBatch(StatementScope statementScope, Connection conn, String sql, Object[] parameters)
      throws SQLException
{
      PreparedStatement ps = null;
      if ((currentSql != null) && (currentSql.equals(sql)))
      {
        int last = statementList.size() - 1;
        ps = (PreparedStatement)statementList.get(last);
      }
      else
      {
        ps = SqlExecutor.prepareStatement(statementScope.getSession(), conn, sql);
        SqlExecutor.setStatementTimeout(statementScope.getStatement(), ps);
        currentSql = sql;
        statementList.add(ps);
        batchResultList.add( new BatchResult(statementScope.getStatement().getId(), sql));
      }
      statementScope.getParameterMap().setParameters(statementScope, ps, parameters);
      ps.addBatch();
      size += 1;
}
在addBatch方法中,就回歸到了JDBC的PreparedStatement,將PreparedStatement對象加入到了Batch的statementList中,以供後面的executeBatch來調用。
 
按理說到這裏爲止,看上去沒什麼問題,思路也比較清晰,可是爲何我加了startBatch和executeBatch以後效率沒有提高呢。咱們還得回到前面SqlMapExecutorDelegate類中的insert方法中去,前面已經分析過了,若是insert操做發現沒有事務,那麼就會使用自動事務,會建立一個事務對象,設置到sessionScope中去,在insert方法的最後幾行,調用了一個autoCommitTransaction方法:
protected void autoCommitTransaction(SessionScope sessionScope, boolean autoStart) throws SQLException
{
    if (autoStart)
      sessionScope.getSqlMapTxMgr().commitTransaction();
}
也就說若是是自動事務管理,那麼在這個方法中就會調用sessionScope中的事務事務管理器(SqlMapClientImpl自己,他實現了 SqlMapTransactionManager接口 )的commitTransaction方法:
public void commitTransaction() throws SQLException {
    getLocalSqlMapSession().commitTransaction();
}

 

他又調用了SqlMapSessionImpl的commitTransaction方法,最中調用到了SqlMapExecutorDelegate的commitTransaction方法:
public void commitTransaction(SessionScope sessionScope) throws SQLException{
    try
    {
      if (sessionScope.isInBatch()) {
        executeBatch(sessionScope);
      }
      this.sqlExecutor.cleanup(sessionScope);
      this.txManager.commit(sessionScope);
    } catch (TransactionException e) {
      throw new NestedSQLException( "Could not commit transaction.  Cause: " + e, e);
    }
}
這裏就能夠看到,若是發現實在批處理中,那麼就直接把批處理執行了再提交。這也就是咱們不加事務的狀況下,使用ibatis的startBatch和executeBatch方法無效的緣由:由於沒一次操做(insert等)添加到批量操做中以後,又在自動事務提交的時候把批量操做給執行了,所以實質上仍是單條單條的操做在執行。
 
使用Spring提供的SqlMapClientTempalte的execute方法執行時,傳入了一個SqlMapClientCallback類型的對象,咱們將執行批處理的代碼寫在了該對象的doInSqlMapClient方法中,這樣就不須要咱們在去寫事務處理代碼了,這時候會發現批處理就生效了。Spring也沒作什麼神奇的事情,就是給SesccionScope設置了一個Transaction對象,致使咱們在執行操做(insert)時,自動事務就不會啓用了。
public Object execute(SqlMapClientCallback action) throws DataAccessException {
   
    SqlMapSession session = this.sqlMapClient.openSession(); //這裏打開了session,後續操做使用這個SqlMapSession對象完成
   
    Connection ibatisCon = null;

    try {
        Connection springCon = null;
        DataSource dataSource = getDataSource();
        boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);

        // Obtain JDBC Connection to operate on...
        try {
            ibatisCon = session.getCurrentConnection();
            if (ibatisCon == null) {
                springCon = (transactionAware ?
                        dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource));
                session.setUserConnection(springCon); //這一步操做很關鍵
            }
        }
        catch (SQLException ex) {
            throw new CannotGetJdbcConnectionException( "Could not get JDBC Connection", ex);
        }

        // Execute given callback...
        try {
            return action.doInSqlMapClient(session); //傳入了打開的SqlSession對象
        }
}
這裏省略部分其餘代碼,僅看看咱們關心的部分。首先建立了一個SqlMapSession,而且獲取了一個Spring可管理的Connection,設置到了SqlMapSession中,正是這一操做使得ibatis的自動事務關閉了。咱們看SqlMapSession的setUserConnection方法,調用了delegate的setUserProviededTransaction方法:
public void setUserProvidedTransaction(SessionScope sessionScope, Connection userConnection)
{
    if (sessionScope.getTransactionState() == TransactionState.STATE_USER_PROVIDED) {
      sessionScope.recallTransactionState();
    }
    if (userConnection != null) {
      Connection conn = userConnection;
      sessionScope.saveTransactionState();
      sessionScope.setTransaction( new UserProvidedTransaction(conn));
      sessionScope.setTransactionState(TransactionState.STATE_USER_PROVIDED);
    } else {
      sessionScope.setTransaction(null);
      sessionScope.closePreparedStatements();
      sessionScope.cleanup();
    }
}
這裏就給SessionScope設置了一個UserProvidedTransactiond對象。ibatis就不會再去關注事務了,由用戶本身去管理事務了,這裏至關於就是把事務交給了spring來管理了。若是咱們沒用經過spring給執行批量操做的方法加事務操做,那麼實際上就至關於這段代碼沒有使用事務。
相關文章
相關標籤/搜索