本文檔地址: 如何開發本身的通用Mapperjava
博客排版不如直接在gitosc上查看,建議去上面的連接查看。git
#前言github
自從發了通用Mapper-0.1.0版本後,我以爲對少數人來講,這多是他們正好須要的一個工具。至少目前的通用DAO中,不多能有比這個更強大的。sql
可是對另外一部分人來講,使用Mybatis代碼生成器(我正在和一些朋友翻譯這個文檔,地址:MyBatis Generator)生成xml很方便,不須要使用通用Mapper。數據庫
實際上若是你沒法在本身的業務中提取出通用的單表(多表實際上能實現,可是限制會增多,不如手寫xml)操做,通用的Mapper除了能增長你的初始效率以及更乾淨的xml配置外,沒有特別大的優點。apache
爲了更方便的擴展通用Mapper,我對0.1.0版本進行了重構。目前已經發布了0.2.0版本,這裏要講如何開發本身須要的通用Mapper。app
#如何開發本身的通用Mapperdom
##要求ide
本身定義的通用Mapper必須包含泛型,例如MysqlMapper<T>
。工具
自定義的通用Mapper接口中的方法須要有合適的註解。具體能夠參考Mapper
須要繼承MapperTemplate
來實現具體的操做方法。
通用Mapper中的Provider
一類的註解只能使用相同的type
類型(這個類型就是第三個要實現的類。)。實際上method
也都寫的同樣。
##HsqldbMapper實例
###第一步,建立HsqldbMapper<T>
public interface HsqldbMapper<T> { }
這個接口就是咱們定義的通用Mapper,具體的接口方法在第三步寫。其餘的Mapper能夠繼承這個HsqldbMapper<T>
。
###第二部,建立HsqldbProvider
public class HsqldbProvider extends MapperTemplate { //繼承父類的方法 public HsqldbProvider(Class<?> mapperClass, MapperHelper mapperHelper) { super(mapperClass, mapperHelper); } }
這個類是實際處理操做的類,須要繼承MapperTemplate
,具體代碼在第四步寫。
###第三步,在HsqldbMapper<T>
中添加通用方法 這裏以一個分頁查詢做爲例子。 public interface HsqldbMapper<T> { /** * 單表分頁查詢 * * @param object * @param offset * @param limit * @return */ @SelectProvider(type=HsqldbProvider.class,method = "dynamicSQL") List<T> selectPage(@Param("entity") T object, @Param("offset") int offset, @Param("limit") int limit); }
返回結果爲List<T>,入參分別爲查詢條件和分頁參數。在Mapper的接口方法中,當有多個入參的時候建議增長@Param
註解,不然就得用param1,param2...
來引用參數。
同時必須在方法上添加註解。查詢使用SelectProvider
,插入使用@InsertProvider
,更新使用UpdateProvider
,刪除使用DeleteProvider
。不一樣的Provider就至關於xml中不一樣的節點,如<select>,<insert>,<update>,<delete>
。
由於這裏是查詢,因此要設置爲SelectProvider
,這4個Provider
中的參數都同樣,只有type
和method
。
type
必須設置爲實際執行方法的HasqldbProvider.class
,method
必須設置爲"dynamicSQL"
。
通用Mapper處理的時候會根據type反射HasqldbProvider
查找方法,而Mybatis的處理機制要求method必須是type
類中只有一個入參,且返回值爲String
的方法。"dynamicSQL"
方法定義在MapperTemplate
中,該方法以下:
public String dynamicSQL(Object record) { return "dynamicSQL"; }
這個方法只是爲了知足Mybatis的要求,沒有任何實際的做用。
###第四步,在HsqldbProvider
中實現真正處理Sql的方法
在這裏有一點要求,那就是HsqldbProvider
處理HsqldbMapper<T>
中的方法時,方法名必須同樣,由於這裏須要經過反射來獲取對應的方法,方法名一致一方面是爲了減小開發人員的配置,另外一方面和接口對應看起來更清晰。
除了方法名必須同樣外,入參必須是MappedStatement ms
,除此以外返回值能夠是void
或者SqlNode
之一。
這裏先講一下通用Mapper的實現原理。通用Mapper目前是經過攔截器在通用方法第一次執行的時候去修改MappedStatement
對象的SqlSource
屬性。並且只會執行一次,之後就和正常的方法沒有任何區別。
使用Provider
註解的這個Mapper方法,Mybatis自己會處理成ProviderSqlSource
(一個SqlSource
的實現類),因爲以前的配置,這個ProviderSqlSource
種的SQL是上面代碼中返回的"dynamicSQL"
。這個SQL沒有任何做用,若是不作任何修改,執行這個代碼確定會出錯。因此在攔截器中攔截符合要求的接口方法,遇到ProviderSqlSource
就經過反射調用如HsqldbProvider
中的具體代碼去修改原有的SqlSource
。
最簡單的處理Mybatis SQL的方法是什麼?就是建立SqlNode
,使用DynamicSqlSource
,這種狀況下咱們不須要處理入參,不須要處理代碼中的各類類型的參數映射。比執行SQL的方式容易不少。
有關這部分的內容建議查看通用Mapper的源碼和Mybatis源碼瞭解,若是不瞭解在這兒說多了反而會亂。
下面在HsqldbProvider
中添加public SqlNode selectPage(MappedStatement ms)
方法:
/** * 分頁查詢 * @param ms * @return */ public SqlNode selectPage(MappedStatement ms) { Class<?> entityClass = getSelectReturnType(ms); //修改返回值類型爲實體類型 setResultType(ms, entityClass); List<SqlNode> sqlNodes = new ArrayList<SqlNode>(); //靜態的sql部分:select column ... from table sqlNodes.add(new StaticTextSqlNode("SELECT " + EntityHelper.getSelectColumns(entityClass) + " FROM " + tableName(entityClass))); //獲取所有列 List<EntityHelper.EntityColumn> columnList = EntityHelper.getColumns(entityClass); List<SqlNode> ifNodes = new ArrayList<SqlNode>(); boolean first = true; //對全部列循環,生成<if test="property!=null">[AND] column = #{property}</if> for (EntityHelper.EntityColumn column : columnList) { StaticTextSqlNode columnNode = new StaticTextSqlNode((first ? "" : " AND ") + column.getColumn() + " = #{entity." + column.getProperty() + "} "); if (column.getJavaType().equals(String.class)) { ifNodes.add(new IfSqlNode(columnNode, "entity."+column.getProperty() + " != null and " + "entity."+column.getProperty() + " != '' ")); } else { ifNodes.add(new IfSqlNode(columnNode, "entity."+column.getProperty() + " != null ")); } first = false; } //將if添加到<where> sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), new MixedSqlNode(ifNodes))); //處理分頁 sqlNodes.add(new IfSqlNode(new StaticTextSqlNode(" LIMIT #{limit}"),"offset==0")); sqlNodes.add(new IfSqlNode(new StaticTextSqlNode(" LIMIT #{limit} OFFSET #{offset} "),"offset>0")); return new MixedSqlNode(sqlNodes); }
注:對這段代碼感受吃力的,能夠對比本頁最下面結構部分XML形式的查看。
首先這段代碼要實現的功能是這樣,根據傳入的實體類參數中不等於null(字符串也不等於'')的屬性做爲查詢條件進行查詢,根據分頁參數進行分頁。
先看這兩行代碼:
//獲取實體類型 Class<?> entityClass = getSelectReturnType(ms); //修改返回值類型爲實體類型 setResultType(ms, entityClass);
首先獲取了實體類型,而後經過setResultType
將返回值類型改成entityClass,就至關於resultType=entityClass
。
**這裏爲何要修改呢?**由於默認返回值是T
,Java並不會自動處理成咱們的實體類,默認狀況下是Object
,對於全部的查詢來講,咱們都須要手動設置返回值類型。
對於insert,update,delete
來講,這些操做的返回值都是int
,因此不須要修改返回結果類型。
以後從List<SqlNode> sqlNodes = new ArrayList<SqlNode>();
代碼開始拼寫SQL,首先是SELECT查詢頭,在EntityHelper.getSelectColumns(entityClass)
中還處理了別名的狀況。
而後獲取全部的列,對列循環建立<if entity.property!=null>column = #{entity.property}</if>
節點。最後把這些if節點組成的List放到一個<where>
節點中。
這一段使用屬性時用的是 entity. + 屬性名
,entity
來自哪兒?來自咱們前面接口定義處的Param("entity")
註解,後面的兩個分頁參數也是。若是你用過Mybatis,相信你能明白。
以後在<where>
節點後添加分頁參數,當offset==0
時和offset>0
時的分頁代碼不一樣。
最後封裝成一個MixedSqlNode
返回。
返回後通用Mapper是怎麼處理的,這裏貼下源碼:
SqlNode sqlNode = (SqlNode) method.invoke(this, ms); DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(ms.getConfiguration(), sqlNode); setSqlSource(ms, dynamicSqlSource);
返回SqlNode
後建立了DynamicSqlSource
,而後修改了ms原來的SqlSource
。
###第五步,配置通用Mapper接口到攔截器插件中
<plugins> <plugin interceptor="com.github.abel533.mapper.MapperInterceptor"> <!--================================================--> <!--可配置參數說明(通常無需修改)--> <!--================================================--> <!--UUID生成策略--> <!--配置UUID生成策略須要使用OGNL表達式--> <!--默認值32位長度:@java.util.UUID@randomUUID().toString().replace("-", "")--> <!--<property name="UUID" value="@java.util.UUID@randomUUID().toString()"/>--> <!--主鍵自增回寫方法,默認值MYSQL,詳細說明請看文檔--> <property name="IDENTITY" value="HSQLDB"/> <!--序列的獲取規則,使用{num}格式化參數,默認值爲{0}.nextval,針對Oracle--> <!--可選參數一共3個,對應0,1,2,分別爲SequenceName,ColumnName,PropertyName--> <property name="seqFormat" value="{0}.nextval"/> <!--主鍵自增回寫方法執行順序,默認AFTER,可選值爲(BEFORE|AFTER)--> <!--<property name="ORDER" value="AFTER"/>--> <!--支持Map類型的實體類,自動將大寫下劃線的Key轉換爲駝峯式--> <!--這個處理使得通用Mapper能夠支持Map類型的實體(實體中的字段必須按常規方式定義,不然沒法反射得到列)--> <property name="cameHumpMap" value="true"/> <!--通用Mapper接口,多個用逗號隔開--> <property name="mappers" value="com.github.abel533.mapper.Mapper,com.github.abel533.hsqldb.HsqldbMapper"/> </plugin> </plugins>
這裏主要是mappers參數:
<property name="mappers" value="com.github.abel533.mapper.Mapper,com.github.abel533.hsqldb.HsqldbMapper"/>
多個通用Mapper能夠用逗號隔開。
##測試
接下來編寫代碼進行測試。
public interface CountryMapper extends Mapper<Country>,HsqldbMapper<Country> { }
在CountryMapper
上增長繼承HsqldbMapper<Country>
。
編寫以下的測試:
@Test public void testDynamicSelectPage() { SqlSession sqlSession = MybatisHelper.getSqlSession(); try { CountryMapper mapper = sqlSession.getMapper(CountryMapper.class); //帶查詢條件的分頁查詢 Country country = new Country(); country.setCountrycode("US"); List<Country> countryList = mapper.selectPage(country, 0, 10); //查詢總數 Assert.assertEquals(1, countryList.size()); //空參數的查詢 countryList = mapper.selectPage(new Country(), 100, 10); Assert.assertEquals(10, countryList.size()); } finally { sqlSession.close(); } }
測試輸出日誌以下:
DEBUG [main] - ==> Preparing: SELECT ID,COUNTRYNAME,COUNTRYCODE FROM COUNTRY WHERE COUNTRYCODE = ? LIMIT ? DEBUG [main] - ==> Parameters: US(String), 10(Integer) TRACE [main] - <== Columns: ID, COUNTRYNAME, COUNTRYCODE TRACE [main] - <== Row: 174, United States of America, US DEBUG [main] - <== Total: 1 DEBUG [main] - ==> Preparing: SELECT ID,COUNTRYNAME,COUNTRYCODE FROM COUNTRY LIMIT ? OFFSET ? DEBUG [main] - ==> Parameters: 10(Integer), 100(Integer) TRACE [main] - <== Columns: ID, COUNTRYNAME, COUNTRYCODE TRACE [main] - <== Row: 101, Maldives, MV TRACE [main] - <== Row: 102, Mali, ML TRACE [main] - <== Row: 103, Malta, MT TRACE [main] - <== Row: 104, Mauritius, MU TRACE [main] - <== Row: 105, Mexico, MX TRACE [main] - <== Row: 106, Moldova, Republic of, MD TRACE [main] - <== Row: 107, Monaco, MC TRACE [main] - <== Row: 108, Mongolia, MN TRACE [main] - <== Row: 109, Montserrat Is, MS TRACE [main] - <== Row: 110, Morocco, MA DEBUG [main] - <== Total: 10
測試沒有任何問題。
這裏在來點很容易實現的一個功能。上面代碼中:
countryList = mapper.selectPage(new Country(), 100, 10);
傳入一個沒有設置任何屬性的Country
的時候會查詢所有結果。有些人會以爲傳入一個空的對象不如傳入一個null
。咱們修改測試代碼看看結果。
執行測試代碼後拋出異常:
Caused by: org.apache.ibatis.ognl.OgnlException: source is null for getProperty(null, "id")
爲何會異常呢,由於咱們上面代碼中直接引用的entity.property
,在引用前並無判斷entity != null
,於是致使了這裏的問題。
咱們修改HsqldbProvider
中的selectPage
方法,將最後幾行代碼進行修改,原來的代碼:
//將if添加到<where> sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), new MixedSqlNode(ifNodes)));
修改後:
//增長entity!=null判斷 IfSqlNode ifSqlNode = new IfSqlNode(new MixedSqlNode(ifNodes),"entity!=null"); //將if添加到<where> sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), ifSqlNode));
以後再進行測試就沒有問題了。
##更多例子
更多例子能夠參考通用Mapper中的Mapper<T>
和MapperProvider
進行參考。代碼量不是很大可是實現了經常使用的這些功能。
當你瞭解了原理以及掌握了SqlNode
的結構後,相信你能寫出更多更強大的通用Mapper。
我曾經說過會根據不一樣的數據庫寫一些針對性的通用Mapper,當我開始考慮重構的時候,我就想,我應該教會須要這個插件的開發人員如何本身實現。
一我的的能力是有限的,並且寫一個東西開源出來給你們用很容易,可是維護不易。因此呢,我但願以爲這篇文檔有用的各位可以分享本身的實現。
我我的若是有時間,我會考慮增長通用的Example
查詢。Example
類的設計比較複雜,對應的SqlNode
結構並非很複雜。若是有人有興趣,我能夠協助開發Example
通用查詢。
##結構
對於剛剛瞭解上述內容的開發人員來講,SqlNode
可能沒有那麼直觀,爲了便於理解。我在這裏將上面最後修改完成的SqlNode以xml的形式寫出來。
<select id="selectPage" resultType="com.github.abel533.model.Country"> SELECT ID,COUNTRYNAME,COUNTRYCODE FROM COUNTRY <where> <if test="entity!=null> <if test="entity.id!=null"> id = #{entity.id} </if> <if test="entity.countryname!=null and entity.countryname!=''"> countryname = #{entity.countryname} </if> <if test="entity.countrycode!=null and entity.countrycode!=''"> countrycode = #{entity.countrycode} </if> </if> </where> <if test="offset==0"> LIMIT #{limit} </if> <if test="offset>0"> LIMIT #{limit} OFFSET #{offset} </if> </select>
看到這個結構,再和上面代碼一一對應應該就不難理解了。熟悉之後,你可能也會以爲JAVA代碼方式處理通用的Mapper會容易不少。