接着昨天的Mybatis扩展——IDaoTemplate接口。
扩展9:批量执行
1、明确什么是批量执行
首先说明一下,这里的批量执行不是利用<foreach>标签生成一长串的sql字符串——这并不是真正的JDBC批量执行,我这里所说的批量是指在内核调用JDBC的addBatch、executeBatch等方法来实现的。类似下面的示例:
1 private void batchUpdate(Connection conn) throws SQLException { 2 PreparedStatement ps = null; 3 try{ 4 String sql = " INSERT INTO BF_PARAM_ENUM_DATA " 5 + " (PARAM_CODE,DATA_CODE,DATA_TEXT,DATA_PARAM,SEQNO,DES) " 6 + " VALUES(?,?,?,?,?,?) "; 7 ps = conn.prepareStatement(sql); 8 for(int i = 0; i < 10; i++){ 9 String random = RandomStringUtils.randomAlphabetic(8); 10 ps.setString(1, "TEST");//PARAM_CODE 11 ps.setString(2, random);//DATA_CODE 12 ps.setString(3, "数据" + random);//DATA_TEXT 13 ps.setString(4, "参数" + random);//DATA_PARAM 14 ps.setInt(5, i);//SEQNO 15 ps.setString(6, "");//MEMO 16 ps.addBatch(); 17 } 18 int[] rs = ps.executeBatch(); 19 System.out.print("batch insert : "); 20 for(int r : rs){ 21 System.out.print(r+","); 22 } 23 System.out.println(); 24 }finally{ 25 try{ 26 ps.close(); 27 }catch(Exception e){} 28 } 29 }
2、批量执行的API
明白了批量执行的含义,接着看批量执行的API,再看一次IDaoTemplate接口中批量执行相关方法:
/** * 执行批量:一个SQL执行多次 * @param sqlId SQL-ID * @param parameters 参数对象数组 * @return 批量执行的影响记录数组 */ public int[] executeBatch(String sqlId, List<?> parameters); /** * 执行批量:一次执行多个SQL * @param sqlIds 要执行的一组SQL-ID * @return 批量执行的影响记录数组 */ public int[] executeBatch(List<String> sqlIds); /** * 执行批量:一次执行多个SQL * @param sqlIds 要执行的一组SQL-ID * @param parameters 参数对象数组 * @return 批量执行的影响记录数组 */ public int[] executeBatch(List<String> sqlIds, List<?> parameters);
简单的理解,批量执行就是需要执行一组操作,要么是相同SQL不同的参数,要么就是不同的SQL(参数相同或不同都可以)。至于这些接口怎么实现,都比较简单,根据SqlSessionFactory,创建批量执行的会话,大概如下:
this.batchSqlSession = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
然后和普通会话一样去执行update/insert/delete等操作,完了再调用批量会话的flushBatch方法返回结果即可。
这里有几个问题:
- 批量执行的事务:批量执行怎么保证事务,是否通过添加Transactional注解即可?事实上,事务注解也只是一个注解,它本身不会对程序运行起任何影响,只有解析这个注解的程序,根据注解属性执行不同的业务逻辑,才会真正体现注解的作用。而另外一方面,Spring注解必须要求调用方法是容器管理的Bean方法,而IDaoTemplate接口的实现类,并不一定是Spring容器中的bean。因此,这里通过事务注解并不能生效,而需要使用编程式事务。相关代码大致如下:
1 SqlSession batchSqlSession = getExecuteSqlSession(); 2 PlatformTransactionManager txManager = this.getTransactinManager(dataSource); 3 if(null == txManager){ 4 for(int i = 0, s = sqlIds.size(); i < s; i++){ 5 batchSqlSession.update(SqlManager.getExecuteSqlId(sqlIds.get(i)), parameters==null?null : parameters.get(i)); 6 } 7 List<BatchResult> r = batchSqlSession.flushStatements(); 8 return resolveBatchResult(r); 9 }else{ 10 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); 11 def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); 12 def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); 13 TransactionStatus status = txManager.getTransaction(def); 14 try { 15 for(int i = 0, s = sqlIds.size(); i < s; i++){ 16 batchSqlSession.update(SqlManager.getExecuteSqlId(sqlIds.get(i)), parameters==null?null : parameters.get(i)); 17 } 18 List<BatchResult> r = batchSqlSession.flushStatements(); 19 txManager.commit(status); 20 return resolveBatchResult(r); 21 } catch (Throwable e) { 22 txManager.rollback(status); 23 throw Throw.createRuntimeException(e); 24 } 25 }
- 执行方式的混合:如果在一个服务方法中,既有一般方式的dao访问,也有批量执行的dao访问,同时还处于一个事务中。由于Mybatis自身的限制,不能在同一个事务中切换执行方法,因此会抛出异常。那怎么办呢?我是利用ThreadLocal变量保存当前线程是否开启了批量执行模式,然后提供了如下三个管理方法作为批量执行的支持接口:
3、批量执行模式管理API
/** * 打开批量执行模式 */ public void openBatchType(); /** * 恢复打开批量执行模式之前的执行模式 */ public void resetBatchType(); /** * 获取批量执行结果 * @return */ public int[] flushBatch();
这样,在前面提到的情况下,可以在服务方法开始,就调用openBatchType打开批量执行方式,然后依次执行各sql脚本,调用flushBatch()获取批量执行结果,在finally快再将当前线程的执行方式切换为调用openBatchType方法之前的执行方式。
批量执行先说到这里,下一次再结合Dao接口的方法签名、Dao接口的自动代理子类等详细说明怎么使用自动代理调用批量执行。
扩展10:merge
在实际业务中,常常会有存在更新、不存在插入的数据库操作——merge,这种操作在Oracle数据库中可以直接利用SQL语法去更新,但是这种方法并不通用,更好的方法就是在Java中添加这个接口方法,但是添加接口方法也面临一个问题:怎么保证事务性?
其实在看过前面的批量执行的事务处理之后,就没什么秘密可言了,类似的使用编程式事务就可以了。
遗憾的是——由于平台组评审过于谨慎,这个接口并没有添加到IDaoTemplate中:
/** * 存在更新、不存在插入 * @param insertSqlId 插入的SQL-ID * @param updateSqlId 更新的SQL-ID * @param parameter 参数对象 * @return 影响的记录条数 */ public int merge(String insertSqlId, String updateSqlId, Object parameter);
类似于merge,还可进一步做更多的扩展,只是通用性比较低,这里举一个例子来说明一下:
/** * 根据条件执行不同sql * <p> * <ul> * <li> 将参数对象parameter作为根对象,对条件表达式caseExpression求值 * <li> 根据求值结果,从sqlId映射中查找对应的sqlId,如果没有找到,就返回默认的sqlId * <li> 根据计算的sqlId和参数对象,执行数据库操作,并返回结果 * </ul> * </p> * @param caseExpression 条件表达式,可为OGNL/SpEL * @param sqlIdMapping sqlId映射 * @param defaultSqlId 默认的sqlId * @param parameter 参数对象 * @return 影响的记录条数 */ public int switchExecute(String caseExpression, Map<String, String> sqlIdMapping, String defaultSqlId, Object parameter);
今天到此为止。