天天看點

如何開發自己的通用Mapper

前言

自從發了通用Mapper-0.1.0版本後,我覺得對少數人來說,這可能是他們正好需要的一個工具。至少目前的通用DAO中,很少能有比這個更強大的。

但是對另一部分人來說,使用Mybatis代碼生成器(我正在和一些朋友翻譯這個文檔,位址:​​MyBatis Generator​​)生成xml很友善,不需要使用通用Mapper。

實際上如果你無法在自己的業務中提取出通用的單表(多表實際上能實作,但是限制會增多,不如手寫xml)操作,通用的Mapper除了能增加你的初始效率以及更幹淨的xml配置外,沒有特别大的優勢。

為了更友善的擴充通用Mapper,我對0.1.0版本進行了重構。目前已經釋出了0.2.0版本,這裡要講如何開發自己需要的通用Mapper。

如何開發自己的通用Mapper

​要求

​MysqlMapper<T>​

  1. 自定義的通用Mapper接口中的方法需要有合适的注解。具體可以參考​​

    ​Mapper​

    ​​
  2. 需要繼承​​

    ​MapperTemplate​

    ​​來實作具體的操作方法。

​Provider​

  1. 一類的注解隻能使用相同的​

    ​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>