论坛首页 Java企业应用论坛

iBatis batch处理那些事

浏览 5708 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2013-02-02   最后修改:2013-02-02
   昨天应同事要求在框架中(Spring+iBatis2.3.4)加入Batch处理,于是满足之,由于需要更灵活并且不想为批量插入、批量更新、批量删除等操作单独写对应的方法,于是写了这样的一个方法
	public Object batchExecute(final CallBack callBack) {
		Object result = getSqlMapClientTemplate().execute(new SqlMapClientCallback<Object>() {

			@Override
			public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
				executor.startBatch();
				Object obj = callBack.execute(new IbatisSqlExecutor(executor));
				executor.executeBatch();
				return obj;
			}
		});
		return result;
	}

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

class IbatisSqlExecutor implements SqlExecutor {
	private SqlMapExecutor executor;
	IbatisSqlExecutor(SqlMapExecutor executor) {
		this.executor = executor;
	}
	@Override
	public Object insert(String id, Object parameterObject) throws SQLException {
		return executor.insert(id, parameterObject);
	}
	// 剩下的就省略了,和insert都类似
}

public interface CallBack {
		Object execute(SqlExecutor executor);
}


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

再然后...尼玛不但速度没变快还异常了,原因竟然是生成了太多的PreparedStatement,你妹不是批处理吗?不是应该一个sql只有一个PreparedStatement吗?
跟踪iBatis代码,发现了iBatis是这样处理的
// 以下代码来自com.ibatis.sqlmap.engine.execution.SqlExecutor$Batch

    public void addBatch(StatementScope statementScope, Connection conn, String sql, Object[] parameters) throws SQLException {
      PreparedStatement ps = null;
// 就是它:currentSql
      if (currentSql != null && currentSql.equals(sql)) {
        int last = statementList.size() - 1;
        ps = (PreparedStatement) statementList.get(last);
      } else {
        ps = prepareStatement(statementScope.getSession(), conn, sql);
        setStatementTimeout(statementScope.getStatement(), ps);
// 就是它:currentSql
        currentSql = sql;
        statementList.add(ps);
        batchResultList.add(new BatchResult(statementScope.getStatement().getId(), sql));
      }
      statementScope.getParameterMap().setParameters(statementScope, ps, parameters);
      ps.addBatch();
      size++;
    }

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

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



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


下面贴出修改后完整的代码,方便有同样需求的同学修改,只贴出内部类com.ibatis.sqlmap.engine.execution.SqlExecutor$Batch,com.ibatis.sqlmap.engine.execution.SqlExecutor没有做出任何修改
	private static class Batch {

		private Map<String, PreparedStatement> statementMap = new HashMap<String, PreparedStatement>();
		private Map<String, BatchResult> batchResultMap = new HashMap<String, BatchResult>();
		private int size;

		/**
		 * Create a new batch
		 */
		public Batch() {
			size = 0;
		}

		/**
		 * Getter for the batch size
		 * @return - the batch size
		 */
		@SuppressWarnings("unused")
		public int getSize() {
			return size;
		}

		/**
		 * Add a prepared statement to the batch
		 * @param statementScope - the request scope
		 * @param conn - the database connection
		 * @param sql - the SQL to add
		 * @param parameters - the parameters for the SQL
		 * @throws SQLException - if the prepare for the SQL fails
		 */
		public void addBatch(StatementScope statementScope, Connection conn, String sql, Object[] parameters) throws SQLException {
			PreparedStatement ps = statementMap.get(sql);
			if (ps == null) {
				ps = prepareStatement(statementScope.getSession(), conn, sql);
				setStatementTimeout(statementScope.getStatement(), ps);
				statementMap.put(sql, ps);
				batchResultMap.put(sql, new BatchResult(statementScope.getStatement().getId(), sql));
			}
			statementScope.getParameterMap().setParameters(statementScope, ps, parameters);
			ps.addBatch();
			++size;
		}

		/**
		 * TODO (Jeff Butler) - maybe this method should be deprecated in some release,
		 * and then removed in some even later release. executeBatchDetailed gives
		 * much more complete information.
		 * <p/>
		 * Execute the current session's batch
		 * @return - the number of rows updated
		 * @throws SQLException - if the batch fails
		 */
		public int executeBatch() throws SQLException {
			int totalRowCount = 0;
			for (Map.Entry<String, PreparedStatement> iter : statementMap.entrySet()) {
				PreparedStatement ps = iter.getValue();
				int[] rowCounts = ps.executeBatch();
				for (int j = 0; j < rowCounts.length; j++) {
					if (rowCounts[j] == Statement.SUCCESS_NO_INFO) {
						// do nothing
					} else if (rowCounts[j] == Statement.EXECUTE_FAILED) {
						throw new SQLException("The batched statement at index " + j + " failed to execute.");
					} else {
						totalRowCount += rowCounts[j];
					}
				}
			}
			return totalRowCount;
		}

		/**
		 * Batch execution method that returns all the information
		 * the driver has to offer.
		 * @return a List of BatchResult objects
		 * @throws BatchException (an SQLException sub class) if any nested
		 *             batch fails
		 * @throws SQLException if a database access error occurs, or the drive
		 *             does not support batch statements
		 * @throws BatchException if the driver throws BatchUpdateException
		 */
		@SuppressWarnings({ "rawtypes", "unchecked" })
		public List executeBatchDetailed() throws SQLException, BatchException {
			List answer = new ArrayList();
			int i = 0;
			for (String sql : statementMap.keySet()) {
				BatchResult br = batchResultMap.get(sql);
				PreparedStatement ps = statementMap.get(sql);
				try {
					br.setUpdateCounts(ps.executeBatch());
				} catch (BatchUpdateException e) {
					StringBuffer message = new StringBuffer();
					message.append("Sub batch number ");
					message.append(i + 1);
					message.append(" failed.");
					if (i > 0) {
						message.append(" ");
						message.append(i);
						message.append(" prior sub batch(s) completed successfully, but will be rolled back.");
					}
					throw new BatchException(message.toString(), e, answer, br.getStatementId(), br.getSql());
				}
				++i;
				answer.add(br);
			}
			return answer;
		}

		/**
		 * Close all the statements in the batch and clear all the statements
		 * @param sessionScope
		 */
		public void cleanupBatch(SessionScope sessionScope) {
			for (Map.Entry<String, PreparedStatement> iter : statementMap.entrySet()) {
				PreparedStatement ps = iter.getValue();
				closeStatement(sessionScope, ps);
			}
			statementMap.clear();
			batchResultMap.clear();
			size = 0;
		}
	}
   发表时间:2013-02-06  
改源码会导致日后维护混乱。
0 请登录后投票
   发表时间:2013-02-06  
Kisses99 写道
改源码会导致日后维护混乱。

额,,,还好吧,我没有动jar包,只是利用classloader加载类的顺序屏蔽了jar包里面的对应的类,原来的class文件还在。必要时直接把修改过的源码拿掉就恢复啦
0 请登录后投票
   发表时间:2013-02-08   最后修改:2013-02-08
你好象把批处理理解错了。。。

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

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

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

我觉得楼主你应该在把自己的需求理清一下,我觉得因为这个你去改源码有点不值当。。。
0 请登录后投票
   发表时间:2013-02-08  
MrLee23 写道
你好象把批处理理解错了。。。

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

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

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

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

嗯,你说的对,我也认同你对批处理的说法,不过你仔细看下我修改后的代码,其实同样是按照你说的批处理去执行的,同样是“是一批一批(相同或相似的东西才叫一批)的进行处理”,只不过是可以多预存几批,ibatis原来是只能预存一批,对吧?
0 请登录后投票
   发表时间:2013-02-08  
MrLee23 写道
你好象把批处理理解错了。。。

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

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

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

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

我只是希望能一次提交多组批处理,在应用代码部分不必分开写,绝对不敢说ibatis的批处理思路是错的,呵呵,至于那个伪代码,项目组中有成员写了类似那样的代码,所以我以这样的方式来避免开发人员因为业务代码写的不好而导致的建立大量的PreparedStatement,改源码可能不是个好的方式,这个我承认,谢谢你的指点,你的观点很有道理
0 请登录后投票
   发表时间:2013-02-08   最后修改:2013-02-08
budairenqin 写道
MrLee23 写道
你好象把批处理理解错了。。。

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

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

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

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

我只是希望能一次提交多组批处理,在应用代码部分不必分开写,绝对不敢说ibatis的批处理思路是错的,呵呵,至于那个伪代码,项目组中有成员写了类似那样的代码,所以我以这样的方式来避免开发人员因为业务代码写的不好而导致的建立大量的PreparedStatement,改源码可能不是个好的方式,这个我承认,谢谢你的指点,你的观点很有道理

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

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

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

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

呵呵,过年了在家没意思,说了一堆废话,你觉得有道理的地方你可以采纳,无道理的你就当废话就行了 :)

但是,我很欣赏你快速处理问题的路子:)
0 请登录后投票
   发表时间:2013-02-08  
MrLee23 写道
budairenqin 写道
MrLee23 写道
你好象把批处理理解错了。。。

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

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

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

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

我只是希望能一次提交多组批处理,在应用代码部分不必分开写,绝对不敢说ibatis的批处理思路是错的,呵呵,至于那个伪代码,项目组中有成员写了类似那样的代码,所以我以这样的方式来避免开发人员因为业务代码写的不好而导致的建立大量的PreparedStatement,改源码可能不是个好的方式,这个我承认,谢谢你的指点,你的观点很有道理

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

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

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

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

呵呵,过年了在家没意思,说了一堆废话,你觉得有道理的地方你可以采纳,无道理的你就当废话就行了 :)

但是,我很欣赏你快速处理问题的路子:)

句句有用,非常感谢:)
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics