iBatis batch处理那些事

昨天应同事要求在框架中(Spring+iBatis2.3.4)加入Batch处理,于是满足之,由于需要更灵活并且不想为批量插入、批量更新、批量删除等操作单独写对应的方法,于是写了这样的一个方法 

Java代码   收藏代码
  1. public Object batchExecute(final CallBack callBack) {  
  2.     Object result = getSqlMapClientTemplate().execute(new SqlMapClientCallback() {  
  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. }  

  14. 不想将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 
    如图: 

    iBatis batch处理那些事_第1张图片  

    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 statementMap = new HashMap();  
    4.     private Map batchResultMap = new HashMap();  
    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.      * 

       

    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 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 iter : statementMap.entrySet()) {  
    114.             PreparedStatement ps = iter.getValue();  
    115.             closeStatement(sessionScope, ps);  
    116.         }  
    117.         statementMap.clear();  
    118.         batchResultMap.clear();  
    119.         size = 0;  
    120.     }  
    121. }  

     评论:改源码会导致日后维护混乱。

    你好象把批处理理解错了。。。 

    抛开程序员的专业解释,咱就用外行人的思路去理解批处理,是一批一批(相同或相似的东西才叫一批)的进行处理。。。 

    而你的理解(从你伪代码上理解的),是一堆的东西(任何东西、anything,甚至丝毫没有关联的东西)放在一块一起执行或处理叫批处理。。。 

    还有,我看了下ibatis的批处理的思路,感觉没错啊。ibatis他不会考虑你的业务需求,它只考虑数据层面上的东西。 

    我觉得楼主你应该在把自己的需求理清一下,我觉得因为这个你去改源码有点不值当。。。


    其实我明白你的想法,因为业务层人员写了不规范代码,导致程序BUG,而且修改起来特别的费劲而且工作量也多。你修改源代码,直接把工作量降到最低。 

    其实这个事情从两方面看: 
    1方面从工作和上层领导角度看:无外乎领导喜欢你去修改源代码,优点:这样节省工作量和成本,缺点:日后你自己维护和上层领导没关系。 

    2方面从编程专业角度看:因为业务逻辑的BUG,你去修改数据层的源码,这样将来如果出现问题,你需要测试两个地方,1是业务逻辑,2是数据层源码。还有,业务逻辑就不能涉及数据层上的东西,一旦涉及必须改掉,这是mvc的理论,也是敏捷开发的思想。还有很多很多不利的因素,不一一例举了。 

    其实,这次你可以侥幸的发现,我修改源码可以把程序完美的跑起来,但是下一次,或是下下一次,你怎么办,你会越来越发现你的程序逐步的步入僵尸代码的阶段,最后你甚至难以维护而不得不重写,其实做我们程序员这个行业,真就是细节决定成败,耐心决定将来。 



    你可能感兴趣的:(java)