忙裏偷閒,繼續上週的話題,記錄Mybatis的擴展。java
擴展5:設置默認的返回結果類型web
你們知道,在Mybatis的sql-mapper配置文件中,咱們須要給<select>元素添加resultType或resultMap屬性,這兩個屬性有且只能有一個。2013年我在作一個系統的時候,由於業務關係,查詢出的結果集字段常常變化,爲了簡化處理,採用map做爲返回數據的載體,而後不得不在絕大多數<select>元素上添加相似 resultType='java.util.HashMap'(Mybatis有HashMap的簡寫形式,這裏爲了更清晰,使用全限定符),因而催生了一個想法,能不能設置默認的返回結果類型?後面通過調試,繼承SqlSessionFactoryBean添加以下代碼實現:sql
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:自動掃描類型簡稱mybatis
仍是在SqlSessionFactoryBean繼承類中,另外實現的一個擴展就是自動掃描類型簡稱。類型簡稱的用法以下:app
(1)在mybatis全局配置文件中添加別名webapp
1 <typeAliases> 2 <typeAlias alias="RoleBean" type="com.forms.beneform4j.webapp.systemmanage.role.bean.RoleBean" /> 3 </typeAliases>
(2)在sql-mapper文件中使用alias。ide
經過添加自動掃描類型簡稱,就能夠將第一段配置去掉,而直接使用別名了。具體實現大概以下所示:函數
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接口中的批量處理相關的擴展。