前言
自從發了通用Mapper-0.1.0版本後,我覺得對少數人來說,這可能是他們正好需要的一個工具。至少目前的通用DAO中,很少能有比這個更強大的。
但是對另一部分人來說,使用Mybatis代碼生成器(我正在和一些朋友翻譯這個文檔,位址:MyBatis Generator)生成xml很友善,不需要使用通用Mapper。
實際上如果你無法在自己的業務中提取出通用的單表(多表實際上能實作,但是限制會增多,不如手寫xml)操作,通用的Mapper除了能增加你的初始效率以及更幹淨的xml配置外,沒有特别大的優勢。
為了更友善的擴充通用Mapper,我對0.1.0版本進行了重構。目前已經釋出了0.2.0版本,這裡要講如何開發自己需要的通用Mapper。
如何開發自己的通用Mapper
要求
MysqlMapper<T>
- 。
- 自定義的通用Mapper接口中的方法需要有合适的注解。具體可以參考
Mapper
- 需要繼承
來實作具體的操作方法。MapperTemplate
Provider
- 一類的注解隻能使用相同的
類型(這個類型就是第三個要實作的類。)。實際上type
也都寫的一樣。method
HsqldbMapper執行個體
第一步,建立HsqldbMapper<T>
public interface HsqldbMapper<T> {
}
HsqldbMapper<T>
。
第二部,建立HsqldbProvider
public class HsqldbProvider extends MapperTemplate {
//繼承父類的方法
public HsqldbProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
}
MapperTemplate
,具體代碼在**第四步**寫。
第三步,在HsqldbMapper<T>中添加通用方法
這裡以一個分頁查詢作為例子。 public interface HsqldbMapper { /** * 單表分頁查詢 * * @param object * @param offset * @param limit * @return */ @SelectProvider(type=HsqldbProvider.class,method = "dynamicSQL") List selectPage(@Param("entity") T object, @Param("offset") int offset, @Param("limit") int limit); }
@Param
注解,否則就得用
param1,param2...
來引用參數。
SelectProvider
,插入使用
@InsertProvider
,更新使用
UpdateProvider
,删除使用
DeleteProvider
。不同的Provider就相當于xml中不同的節點,如
<select>,<insert>,<update>,<delete>
。
SelectProvider
,這4個
Provider
中的參數都一樣,隻有
type
和
method
。
type
必須設定為實際執行方法的
HasqldbProvider.class
,
method
必須設定為
"dynamicSQL"
。
HasqldbProvider
查找方法,而Mybatis的處理機制要求method必須是
type
類中隻有一個入參,且傳回值為
String
的方法。
"dynamicSQL"
方法定義在
MapperTemplate
中,該方法如下:
public String dynamicSQL(Object record) {
return "dynamicSQL";
}
這個方法隻是為了滿足Mybatis的要求,沒有任何實際的作用。
第四步,在HsqldbProvider中實作真正處理Sql的方法
HsqldbProvider
處理
HsqldbMapper<T>
中的方法時,方法名必須一樣,因為這裡需要通過反射來擷取對應的方法,方法名一緻一方面是為了減少開發人員的配置,另一方面和接口對應看起來更清晰。
MappedStatement ms
,除此之外傳回值可以是
void
或者
SqlNode
之一。
MappedStatement
對象的
SqlSource
屬性。而且隻會執行一次,以後就和正常的方法沒有任何差別。
Provider
注解的這個Mapper方法,Mybatis本身會處理成
ProviderSqlSource
(一個
SqlSource
的實作類),由于之前的配置,這個
ProviderSqlSource
種的SQL是上面代碼中傳回的
"dynamicSQL"
。這個SQL沒有任何作用,如果不做任何修改,執行這個代碼肯定會出錯。是以在攔截器中攔截符合要求的接口方法,遇到
ProviderSqlSource
就通過反射調用如
HsqldbProvider
中的具體代碼去修改原有的
SqlSource
。
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<T>
和
MapperProvider
進行參考。代碼量不是很大但是實作了常用的這些功能。
SqlNode
的結構後,相信你能寫出更多更強大的通用Mapper。
我曾經說過會根據不同的資料庫寫一些針對性的通用Mapper,當我開始考慮重構的時候,我就想,我應該教會需要這個插件的開發人員如何自己實作。
一個人的能力是有限的,而且寫一個東西開源出來給大家用很容易,但是維護不易。是以呢,我希望覺得這篇文檔有用的各位能夠分享自己的實作。
Example
查詢。
Example
類的設計比較複雜,對應的
SqlNode
結構并不是很複雜。如果有人有興趣,我可以協助開發
Example
通用查詢。
結構
<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">
countryname = #{entity.countryname}
</if>
<if test="entity.countrycode!=null">
countrycode = #{entity.countrycode}
</if>
</if>
</where>
<if test="offset==0">
LIMIT #{limit}
</if>
<if test="offset>0">
LIMIT #{limit} OFFSET #{offset}
</if>
</select>