Java EE开发平台随手记3——Mybatis扩展2
忙里偷闲,继续上周的话题,记录Mybatis的扩展。
扩展5:设置默认的返回结果类型
大家知道,在Mybatis的sql-mapper配置文件中,我们需要给<select>元素添加resultType或resultMap属性,这两个属性有且只能有一个。2013年我在做一个系统的时候,因为业务关系,查询出的结果集字段经常变化,为了简化处理,采用map作为返回数据的载体,然后不得不在绝大多数<select>元素上添加类似 resultType='java.util.HashMap'(Mybatis有HashMap的简写形式,这里为了更清晰,使用全限定符),于是催生了一个想法,能不能设置默认的返回结果类型?后面经过调试,继承SqlSessionFactoryBean添加如下代码实现:
1 /** 2 * 设置默认的查询结果返回类型 3 * @param configuration 4 * @param cls 5 * @throws Exception 6 */ 7 private void setDefaultResultType(Configuration configuration, Class<?> cls) throws Exception{ 8 try { 9 Field resultMaps = MappedStatement.class.getDeclaredField("resultMaps"); 10 resultMaps.setAccessible(true); 11 for(Iterator<MappedStatement> i = configuration.getMappedStatements().iterator(); i.hasNext();){ 12 Object o = i.next(); 13 /** 14 * 这里添加类型判断,是因为Mybatis实现中还存放了Ambiguity对象(sql-id的最后一段id重复情况下) 15 */ 16 if(o instanceof MappedStatement){ 17 MappedStatement ms = (MappedStatement)o; 18 if(SqlCommandType.SELECT.equals(ms.getSqlCommandType()) && ms.getResultMaps().isEmpty()){ 19 ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration,ms.getId()+"-Inline",cls,new ArrayList<ResultMapping>(),null); 20 ResultMap resultMap = inlineResultMapBuilder.build(); 21 List<ResultMap> rm = new ArrayList<ResultMap>(); 22 rm.add(resultMap); 23 resultMaps.set(ms, Collections.unmodifiableList(rm)); 24 }else{ 25 } 26 } 27 } 28 } catch (Exception e) { 29 e.printStackTrace(); 30 throw e; 31 } 32 }
这个实现有很多写死的代码,也没有做足够完备的测试,不过到目前为止,总算还没有出错。
我设置的默认返回结果类型是map,当然,这里其实可以更进一步扩展,添加一个SqlID模式和默认结果类型的映射接口,然后就根据需要去实现这个映射关系了。
扩展6:自动扫描类型简称
还是在SqlSessionFactoryBean继承类中,另外实现的一个扩展就是自动扫描类型简称。类型简称的用法如下:
(1)在mybatis全局配置文件中添加别名
1 <typeAliases> 2 <typeAlias alias="RoleBean" type="com.forms.beneform4j.webapp.systemmanage.role.bean.RoleBean" /> 3 </typeAliases>
(2)在sql-mapper文件中使用alias。
通过添加自动扫描类型简称,就可以将第一段配置去掉,而直接使用别名了。具体实现大概如下所示:
1 private void scanTypeAliases(){ 2 if (this.autoScanTypeAliases && hasLength(this.typeAliasesScanPackage) && null != baseClass) { 3 String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesScanPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); 4 List<Class<?>> list = new ArrayList<Class<?>>(); 5 List<String> alias = new ArrayList<String>(); 6 MetaObject meta = SystemMetaObject.forObject(this); 7 Class<?>[] orig = (Class<?>[])meta.getValue("typeAliases"); 8 if(null != orig) 9 { 10 for(Class<?> t : orig){ 11 list.add(t); 12 alias.add(t.getSimpleName().toLowerCase()); 13 } 14 } 15 for (String packageToScan : typeAliasPackageArray) { 16 for(Class<?> type : CoreUtils.scanClassesByParentCls(packageToScan, baseClass)){ 17 String a = type.getSimpleName().toLowerCase(); 18 if (!alias.contains(a)) { 19 list.add(type); 20 alias.add(a); 21 }else{ 22 CommonLogger.warn("Mybatis在自动扫描注册别名时,发现有多个可简写为"+type.getSimpleName()+"的类,将取第一个类,忽略"+type); 23 } 24 } 25 } 26 super.setTypeAliases(list.toArray(new Class<?>[list.size()])); 27 } 28 }
这里属性autoScanTypeAliases表示是否需要自动扫描,typeAliasesScanPackage表示扫描的包,baseClass表示扫描的接口或父类。
我们使用Mybatis,可以在父类中注入SqlSessionTemplate,然后子类调用相关方法,也可以通过写Dao的接口,让mybatis自动生成动态代理类,还可以编写一个静态帮助类,在这个帮助类中注入SqlSessionTemplate,然后提供相应的静态方法。这三种方法,以前用的多的是第三种方法,而现在,因为要让其他同事更容易接受,模块划分更清晰,采用了第二种方法。但这三种方法都有一个特点,那就是只使用Mybatis的SqlSession接口的原生方法。不能直接调用批处理、存储过程等,于是,我在SqlSession基础上,添加了一个IDaoTemplate接口:
1 public interface IDaoTemplate{ 2 3 /** 4 * 查询单笔数据 5 * @param sqlId SQL-ID 6 * @return 单个对象 7 */ 8 public <T> T selectOne(String sqlId); 9 10 /** 11 * 查询单笔数据 12 * @param sqlId SQL-ID 13 * @param parameter 参数对象 14 * @return 单个对象 15 */ 16 public <T> T selectOne(String sqlId, Object parameter); 17 18 /** 19 * 查询列表数据 20 * @param sqlId SQL-ID 21 * @return 对象列表 22 */ 23 public <E> List<E> selectList(String sqlId); 24 25 /** 26 * 查询列表数据 27 * @param sqlId SQL-ID 28 * @param parameter 参数对象 29 * @return 对象列表 30 */ 31 public <E> List<E> selectList(String sqlId, Object parameter); 32 33 /** 34 * 查询分页列表数据 35 * @param sqlId SQL-ID 36 * @param page 分页对象 37 * @return 指定页的对象列表 38 */ 39 public <E> List<E> selectList(String sqlId, IPage page); 40 41 /** 42 * 查询分页列表数据 43 * @param sqlId SQL-ID 44 * @param parameter 参数对象 45 * @param page 分页对象 46 * @return 指定页的对象列表 47 */ 48 public <E> List<E> selectList(String sqlId, Object parameter, IPage page); 49 50 /** 51 * 流式查询 52 * @param sqlId SQL-ID 53 * @return 流式操作接口 54 */ 55 public <E>IListStreamReader<E> selectListStream(String sqlId); 56 57 /** 58 * 流式查询 59 * @param sqlId SQL-ID 60 * @param parameter 参数对象 61 * @return 流式操作接口 62 */ 63 public <E>IListStreamReader<E> selectListStream(String sqlId, Object parameter); 64 65 /** 66 * 流式查询 67 * @param sqlId SQL-ID 68 * @param fetchSize 每次读取的记录条数(0, 5000] 69 * @return 流式操作接口 70 */ 71 public <E>IListStreamReader<E> selectListStream(String sqlId, int fetchSize); 72 73 /** 74 * 流式查询 75 * @param sqlId SQL-ID 76 * @param parameter 参数对象 77 * @param fetchSize 每次读取的记录条数(0, 5000] 78 * @return 流式操作接口 79 */ 80 public <E>IListStreamReader<E> selectListStream(String sqlId, Object parameter, int fetchSize); 81 82 /** 83 * 新增 84 * @param sqlId SQL-ID 85 * @return 影响的记录条数 86 */ 87 public int insert(String sqlId); 88 89 /** 90 * 新增 91 * @param sqlId SQL-ID 92 * @param parameter 参数对象 93 * @return 影响的记录条数 94 */ 95 public int insert(String sqlId, Object parameter); 96 97 /** 98 * 修改 99 * @param sqlId SQL-ID 100 * @return 影响的记录条数 101 */ 102 public int update(String sqlId); 103 104 /** 105 * 修改 106 * @param sqlId SQL-ID 107 * @param parameter 参数对象 108 * @return 影响的记录条数 109 */ 110 public int update(String sqlId, Object parameter); 111 112 /** 113 * 删除 114 * @param sqlId SQL-ID 115 * @return 影响的记录条数 116 */ 117 public int delete(String sqlId); 118 119 /** 120 * 删除 121 * @param sqlId SQL-ID 122 * @param parameter 参数对象 123 * @return 影响的记录条数 124 */ 125 public int delete(String sqlId, Object parameter); 126 127 /** 128 * 执行批量:一个SQL执行多次 129 * @param sqlId SQL-ID 130 * @param parameters 参数对象数组 131 * @return 批量执行的影响记录数组 132 */ 133 public int[] executeBatch(String sqlId, List<?> parameters); 134 135 /** 136 * 执行批量:一次执行多个SQL 137 * @param sqlIds 要执行的一组SQL-ID 138 * @return 批量执行的影响记录数组 139 */ 140 public int[] executeBatch(List<String> sqlIds); 141 142 /** 143 * 执行批量:一次执行多个SQL 144 * @param sqlIds 要执行的一组SQL-ID 145 * @param parameters 参数对象数组 146 * @return 批量执行的影响记录数组 147 */ 148 public int[] executeBatch(List<String> sqlIds, List<?> parameters); 149 150 /** 151 * 打开批量执行模式 152 */ 153 public void openBatchType(); 154 155 /** 156 * 恢复打开批量执行模式之前的执行模式 157 */ 158 public void resetBatchType(); 159 160 /** 161 * 获取批量执行结果 162 * @return 163 */ 164 public int[] flushBatch(); 165 166 /** 167 * 调用存储过程 168 * @param sqlId SQL-ID 169 * @return 存储过程返回结果接口 170 */ 171 public ICallResult call(String sqlId); 172 173 /** 174 * 调用存储过程 175 * @param sqlId SQL-ID 176 * @param parameter 参数对象 177 * @return 存储过程返回结果接口 178 */ 179 public ICallResult call(String sqlId, Object parameter); 180 }
可以看到,其中部分是简单调用SqlSession接口,但也有部分是我们的扩展。
扩展7:流式查询
流式查询有四个重置方法,sql-Id是必须的参数,查询参数parameter和每次处理的记录条数fetchSize是可选的。流式查询的结果接口如下:
1 public interface IListStreamReader<T> { 2 3 /** 4 * 读取当前批次的数据列表,如果没有数据,返回null 5 * @return 当前批次的数据列表 6 */ 7 public List<T> read(); 8 9 /** 10 * 重置读取批次 11 */ 12 public void reset(); 13 }
只有两个方法,其中关键方法是获取当前批次的数据结果集,辅助方法是重置读取批次。
流式查询本质上并没有执行查询,而只是将查询需要的要素包装成为一个对象,当调用者调用这个对象的read方法时,才真正执行数据库查询,而执行查询又使用实现内中内置的分页对象,每次读取只读取当前批次(当前页数)的结果集,查询之后,就内置分页对象的当前页数指向下一页。
流式查询适用于大数据量的查询处理,比如大数据量的数据需要生成Excel文件供客户端下载,一次性查询很容易内存溢出,使用流式查询就可以很好的解决这个问题。
把流式查询结果对象的抽象实现贴在这里,应该更便于理解:
1 public abstract class AbstractListStreamReader<T> implements IListStreamReader<T>{ 2 3 /** 4 * 默认的每次读取记录数 5 */ 6 private static final int defaultFetchSize = 1000; 7 8 /** 9 * 最大的每次读取记录数 10 */ 11 private static final int maxFetchSize = 5000; 12 13 /** 14 * 实际的每次读取记录数 15 */ 16 private final int fetchSize; 17 18 /** 19 * 分页对象 20 */ 21 private final IPage page; 22 23 /** 24 * 是否完成的标志 25 */ 26 private transient boolean finish = false;//是否完成 27 28 /** 29 * 无参构造函数 30 */ 31 public AbstractListStreamReader() { 32 this(defaultFetchSize); 33 } 34 35 /** 36 * 使用指定读取数大小的构造函数 37 * @param fetchSize 每次读取的记录条数 38 */ 39 public AbstractListStreamReader(int fetchSize) { 40 if(fetchSize <= 0 || fetchSize > maxFetchSize){ 41 Throw.throwRuntimeException(DaoExceptionCodes.BF020012, fetchSize, "(0, "+maxFetchSize+"]"); 42 } 43 this.fetchSize = fetchSize; 44 BasePage page = new BasePage(); 45 page.setPageSize(fetchSize); 46 this.page = page; 47 } 48 49 /** 50 * 读取当前批次的列表数据,读取的时候会加锁 51 */ 52 @Override 53 public synchronized List<T> read() { 54 if(!finish){ 55 List<T> rs = doRead(page);//查询当前页数据 56 if(page.hasNextPage()){//有下一页,游标指向下一页 57 page.setPageProperty(page.getTotalRecords(), page.getCurrentPage()+1, fetchSize); 58 }else{//没有下一页,完成 59 finish = true; 60 } 61 return rs; 62 } 63 return null; 64 } 65 66 /** 67 * 执行实际的读取操作 68 * @param page 分页对象 69 * @return 和分页对象相对应的数据记录列表 70 */ 71 abstract protected List<T> doRead(IPage page); 72 73 /** 74 * 重置读取批次,重置过程中会加锁 75 */ 76 @Override 77 public synchronized void reset(){ 78 this.finish = false; 79 this.page.setPageProperty(this.page.getTotalPages(), 1, fetchSize); 80 } 81 }
至于具体的实现,只要继承抽象实现,然后实现
abstract protected List<T> doRead(IPage page);
就可以了,而这个方法只是一个简单的分页查询,实现起来没有任何难度。
扩展8:调用存储过程
Mybatis中可以调用存储过程,但直接使用并不方便,我们将其封装如下:
1 public interface IDaoTemplate{ 2 3 /** 4 * 这里省略了其它方法 5 */ 6 7 8 /** 9 * 调用存储过程 10 * @param sqlId SQL-ID 11 * @return 存储过程返回结果接口 12 */ 13 public ICallResult call(String sqlId); 14 15 /** 16 * 调用存储过程 17 * @param sqlId SQL-ID 18 * @param parameter 参数对象 19 * @return 存储过程返回结果接口 20 */ 21 public ICallResult call(String sqlId, Object parameter); 22 }
这样调用就非常方便了,那么这里的ICallResult是什么呢?看一下它的定义:
1 public interface ICallResult { 2 3 /** 4 * 获取存储过程返回值 5 * @return 6 */ 7 public <T> T getResult(); 8 9 /** 10 * 根据参数名称返回输出参数 11 * @param name 输出参数名称 12 * @return 和输出参数名称相对应的返回结果,如果不存在输出参数,抛出平台运行时异常 13 */ 14 public <T> T getOutputParam(String name); 15 16 /** 17 * 返回输出参数名称的迭代器 18 * @return 输出参数名迭代器 19 */ 20 public Iterator<String> iterator(); 21 }
使用过Mybatis调用存储过程的朋友,看了这个借口应该就能明白,但鉴于存储过程调用并不普通,这里举一个例子:
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 3 <mapper namespace="com.forms.beneform4j.core.dao.mybatis.mapper.call.ICallDao"> 4 5 <select id="call" statementType="CALLABLE"> 6 {call BF_TEST_PACKAGE.BF_TEST_PROCEDURE( 7 #{input, jdbcType=VARCHAR}, 8 #{output1, mode=OUT, jdbcType=VARCHAR}, 9 #{output2, mode=OUT, jdbcType=VARCHAR}, 10 #{rs1, mode=OUT, jdbcType=CURSOR}, 11 #{rs2, mode=OUT, jdbcType=CURSOR} 12 )} 13 </select> 14 </mapper>
如上配置,传入sqlId和参数对象(含input属性)后,返回的ICallResult接口中,可以通过如下的方式获取存储过程的返回值(如果有)和输出参数:
1 @Repository 2 interface ICallDao { 3 4 public ICallResult call(@Param("input")String input); 5 } 6 7 @Service 8 public class ICallDaoTest { 9 10 @Autowired 11 private ICallDao dao; 12 13 @Test 14 public void call() throws Exception { 15 ICallResult rs = dao.call("1"); 16 //直接访问返回结果和输出参数 17 Object returnValue = rs.getResult(); 18 Object output1 = rs.getOutputParam("output1"); 19 List<Object> rs1 = rs.getOutputParam("rs1"); 20 21 //循环访问输出参数 22 Iterator<String> i = rs.iterator(); 23 String name = ""; 24 while(i.hasNext()){ 25 name = i.next(); 26 System.out.println(name + "============" + rs.getOutputParam(name)); 27 } 28 } 29 }
说完了调用存储过程的用法,回过头来简单的提一下调用存储过程的实现:实际上很简单,只要添加一个Mybatis的拦截器即可,拦截结果处理接口ResultSetHandler的方法handleOutputParameters,然后将返回结果和输出参数包装到一个Map对象中即可,具体代码就不贴了。
时间关系,今天写到这里。下次再继续写Dao接口中SqlID的重定向、IDaoTemplate接口中的批量处理相关的扩展。
郴江幸自绕郴山,为谁流下潇湘去?
欲将心事付瑶琴,知音少,弦断有谁听?
倩何人,唤取红巾翠袖,揾英雄泪!
零落成泥碾作尘,只有香如故!