iBatis batch处理那些事

来源:互联网 发布:方舟2017优化好了吗 编辑:程序博客网 时间:2024/05/22 06:05

   昨天应同事要求在框架中(Spring+iBatis2.3.4)加入Batch处理,于是满足之,由于需要更灵活并且不想为批量插入、批量更新、批量删除等操作单独写对应的方法,于是写了这样的一个方法 
Java代码  收藏代码
  1. public Object batchExecute(final CallBack callBack) {  
  2.     Object result = getSqlMapClientTemplate().execute(new SqlMapClientCallback<Object>() {  
  3.   
  4.         @Override  
  5.         public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {  
  6.             executor.startBatch();  
  7.             Object obj = callBack.execute(new IbatisSqlExecutor(executor));  
  8.             executor.executeBatch();  
  9.             return obj;  
  10.         }  
  11.     });  
  12.     return result;  
  13. }  

不想将SqlMapExecutor侵入到业务代码中,于是又有了如下3个类,在今天的主题中不是关键,可以忽略,只是为了将代码贴完整 
Java代码  收藏代码
  1. public interface SqlExecutor {  
  2.     Object insert(String id, Object parameterObject) throws SQLException;  
  3.     Object insert(String id) throws SQLException;  
  4.     int update(String id, Object parameterObject) throws SQLException;  
  5.     int update(String id) throws SQLException;  
  6.     int delete(String id, Object parameterObject) throws SQLException;  
  7.     int delete(String id) throws SQLException;  
  8.     Object queryForObject(String id, Object parameterObject) throws SQLException;  
  9.     Object queryForObject(String id) throws SQLException;  
  10.     Object queryForObject(String id, Object parameterObject, Object resultObject) throws SQLException;  
  11. }  

Java代码  收藏代码
  1. class IbatisSqlExecutor implements SqlExecutor {  
  2.     private SqlMapExecutor executor;  
  3.     IbatisSqlExecutor(SqlMapExecutor executor) {  
  4.         this.executor = executor;  
  5.     }  
  6.     @Override  
  7.     public Object insert(String id, Object parameterObject) throws SQLException {  
  8.         return executor.insert(id, parameterObject);  
  9.     }  
  10.     // 剩下的就省略了,和insert都类似  
  11. }  

Java代码  收藏代码
  1. public interface CallBack {  
  2.         Object execute(SqlExecutor executor);  
  3. }  


然后执行了一个类似以下伪代码行为的操作: 
Java代码  收藏代码
  1. getDao().batchExecute(new CallBack() {  
  2.         @Override  
  3.         public Object execute(SqlExecutor executor) {  
  4.                 for (int i = 0; i < 10000; ++i) {  
  5.                         // 注意每个sql_id的sql语句都是不相同的  
  6.                         executor.insert("id1", obj1);  
  7.                         executor.insert("id2", obj2);  
  8.                         // ...  
  9.                         executor.insert("idn", objn);  
  10.                 }  
  11.                 return null;  
  12.         }  
  13. });  

再然后...尼玛不但速度没变快还异常了,原因竟然是生成了太多的PreparedStatement,你妹不是批处理吗?不是应该一个sql只有一个PreparedStatement吗? 
跟踪iBatis代码,发现了iBatis是这样处理的 
Java代码  收藏代码
  1. // 以下代码来自com.ibatis.sqlmap.engine.execution.SqlExecutor$Batch  
  2.   
  3.     public void addBatch(StatementScope statementScope, Connection conn, String sql, Object[] parameters) throws SQLException {  
  4.       PreparedStatement ps = null;  
  5. // 就是它:currentSql  
  6.       if (currentSql != null && currentSql.equals(sql)) {  
  7.         int last = statementList.size() - 1;  
  8.         ps = (PreparedStatement) statementList.get(last);  
  9.       } else {  
  10.         ps = prepareStatement(statementScope.getSession(), conn, sql);  
  11.         setStatementTimeout(statementScope.getStatement(), ps);  
  12. // 就是它:currentSql  
  13.         currentSql = sql;  
  14.         statementList.add(ps);  
  15.         batchResultList.add(new BatchResult(statementScope.getStatement().getId(), sql));  
  16.       }  
  17.       statementScope.getParameterMap().setParameters(statementScope, ps, parameters);  
  18.       ps.addBatch();  
  19.       size++;  
  20.     }  

不细解释了,只看currentSql这个实例变量就知道了,如果sql与前一次不同那么会新建一个PreparedStatement,所以刚才的伪代码应该这样写: 
Java代码  收藏代码
  1. getDao().batchExecute(new CallBack() {  
  2.         @Override  
  3.         public Object execute(SqlExecutor executor) {  
  4.                 for (int i = 0; i < 10000; ++i) {  
  5.                         executor.insert("id1", obj1);  
  6.                 }  
  7.                 for (int i = 0; i < 10000; ++i) {  
  8.                         executor.insert("id2", obj2);  
  9.                 }  
  10.                 // ...你就反复写for循环吧  
  11.                 return null;  
  12.         }  
  13. });  

很不爽是不?反正我是决了一个定,改iBatis的源码 
改源码最好这么来: 
1.复制对应类的源码到工程下,本例中要复制的是com.ibatis.sqlmap.engine.execution.SqlExecutor 
注意要保持包名,其实是类的全限定名称要一样哇,这样根据ClassLoader的类加载机制会先加载你工程中的SqlExecutor而不加载iBatis jar包中的对应SqlExecutor 
如图: 

 

2.改之,只改static class Batch这个内部类即可,策略是去掉currentSql,将PreparedStatement放入HashMap 
如下: 
Java代码  收藏代码
  1. public void addBatch(StatementScope statementScope, Connection conn, String sql, Object[] parameters) throws SQLException {  
  2.             PreparedStatement ps = statementMap.get(sql);  
  3.             if (ps == null) {  
  4.                 ps = prepareStatement(statementScope.getSession(), conn, sql);  
  5.                 setStatementTimeout(statementScope.getStatement(), ps);  
  6.                 statementMap.put(sql, ps);  
  7.                 batchResultMap.put(sql, new BatchResult(statementScope.getStatement().getId(), sql));  
  8.             }  
  9.             statementScope.getParameterMap().setParameters(statementScope, ps, parameters);  
  10.             ps.addBatch();  
  11.             ++size;  
  12.         }  


下面贴出修改后完整的代码,方便有同样需求的同学修改,只贴出内部类com.ibatis.sqlmap.engine.execution.SqlExecutor$Batch,com.ibatis.sqlmap.engine.execution.SqlExecutor没有做出任何修改 
Java代码  收藏代码
  1. private static class Batch {  
  2.   
  3.     private Map<String, PreparedStatement> statementMap = new HashMap<String, PreparedStatement>();  
  4.     private Map<String, BatchResult> batchResultMap = new HashMap<String, BatchResult>();  
  5.     private int size;  
  6.   
  7.     /** 
  8.      * Create a new batch 
  9.      */  
  10.     public Batch() {  
  11.         size = 0;  
  12.     }  
  13.   
  14.     /** 
  15.      * Getter for the batch size 
  16.      * @return - the batch size 
  17.      */  
  18.     @SuppressWarnings("unused")  
  19.     public int getSize() {  
  20.         return size;  
  21.     }  
  22.   
  23.     /** 
  24.      * Add a prepared statement to the batch 
  25.      * @param statementScope - the request scope 
  26.      * @param conn - the database connection 
  27.      * @param sql - the SQL to add 
  28.      * @param parameters - the parameters for the SQL 
  29.      * @throws SQLException - if the prepare for the SQL fails 
  30.      */  
  31.     public void addBatch(StatementScope statementScope, Connection conn, String sql, Object[] parameters) throws SQLException {  
  32.         PreparedStatement ps = statementMap.get(sql);  
  33.         if (ps == null) {  
  34.             ps = prepareStatement(statementScope.getSession(), conn, sql);  
  35.             setStatementTimeout(statementScope.getStatement(), ps);  
  36.             statementMap.put(sql, ps);  
  37.             batchResultMap.put(sql, new BatchResult(statementScope.getStatement().getId(), sql));  
  38.         }  
  39.         statementScope.getParameterMap().setParameters(statementScope, ps, parameters);  
  40.         ps.addBatch();  
  41.         ++size;  
  42.     }  
  43.   
  44.     /** 
  45.      * TODO (Jeff Butler) - maybe this method should be deprecated in some release, 
  46.      * and then removed in some even later release. executeBatchDetailed gives 
  47.      * much more complete information. 
  48.      * <p/> 
  49.      * Execute the current session's batch 
  50.      * @return - the number of rows updated 
  51.      * @throws SQLException - if the batch fails 
  52.      */  
  53.     public int executeBatch() throws SQLException {  
  54.         int totalRowCount = 0;  
  55.         for (Map.Entry<String, PreparedStatement> iter : statementMap.entrySet()) {  
  56.             PreparedStatement ps = iter.getValue();  
  57.             int[] rowCounts = ps.executeBatch();  
  58.             for (int j = 0; j < rowCounts.length; j++) {  
  59.                 if (rowCounts[j] == Statement.SUCCESS_NO_INFO) {  
  60.                     // do nothing  
  61.                 } else if (rowCounts[j] == Statement.EXECUTE_FAILED) {  
  62.                     throw new SQLException("The batched statement at index " + j + " failed to execute.");  
  63.                 } else {  
  64.                     totalRowCount += rowCounts[j];  
  65.                 }  
  66.             }  
  67.         }  
  68.         return totalRowCount;  
  69.     }  
  70.   
  71.     /** 
  72.      * Batch execution method that returns all the information 
  73.      * the driver has to offer. 
  74.      * @return a List of BatchResult objects 
  75.      * @throws BatchException (an SQLException sub class) if any nested 
  76.      *             batch fails 
  77.      * @throws SQLException if a database access error occurs, or the drive 
  78.      *             does not support batch statements 
  79.      * @throws BatchException if the driver throws BatchUpdateException 
  80.      */  
  81.     @SuppressWarnings({ "rawtypes""unchecked" })  
  82.     public List executeBatchDetailed() throws SQLException, BatchException {  
  83.         List answer = new ArrayList();  
  84.         int i = 0;  
  85.         for (String sql : statementMap.keySet()) {  
  86.             BatchResult br = batchResultMap.get(sql);  
  87.             PreparedStatement ps = statementMap.get(sql);  
  88.             try {  
  89.                 br.setUpdateCounts(ps.executeBatch());  
  90.             } catch (BatchUpdateException e) {  
  91.                 StringBuffer message = new StringBuffer();  
  92.                 message.append("Sub batch number ");  
  93.                 message.append(i + 1);  
  94.                 message.append(" failed.");  
  95.                 if (i > 0) {  
  96.                     message.append(" ");  
  97.                     message.append(i);  
  98.                     message.append(" prior sub batch(s) completed successfully, but will be rolled back.");  
  99.                 }  
  100.                 throw new BatchException(message.toString(), e, answer, br.getStatementId(), br.getSql());  
  101.             }  
  102.             ++i;  
  103.             answer.add(br);  
  104.         }  
  105.         return answer;  
  106.     }  
  107.   
  108.     /** 
  109.      * Close all the statements in the batch and clear all the statements 
  110.      * @param sessionScope 
  111.      */  
  112.     public void cleanupBatch(SessionScope sessionScope) {  
  113.         for (Map.Entry<String, PreparedStatement> iter : statementMap.entrySet()) {  
  114.             PreparedStatement ps = iter.getValue();  
  115.             closeStatement(sessionScope, ps);  
  116.         }  
  117.         statementMap.clear();  
  118.         batchResultMap.clear();  
  119.         size = 0;  
  120.     }  
  121. }  

0 0