教你如何開發Mybatis的通用Mapper

本文檔地址: 如何開發本身的通用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

  1. 本身定義的通用Mapper必須包含泛型,例如MysqlMapper<T>工具

  2. 自定義的通用Mapper接口中的方法須要有合適的註解。具體能夠參考Mapper

  3. 須要繼承MapperTemplate來實現具體的操做方法。

  4. 通用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中的參數都同樣,只有typemethod

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會容易不少。

相關文章
相關標籤/搜索