本文會先介紹通用 mapper 的簡單原理,然后使用最簡單的代碼來實現這個過程。
基本原理
通用 mapper 提供了一些通用的方法,這些通用方法是以接口的形式提供的,例如。
1
2
3
4
5
6
7
|
public interface selectmapper<t> { /** * 根據實體中的屬性值進行查詢,查詢條件使用等號 */ @selectprovider (type = baseselectprovider. class , method = "dynamicsql" ) list<t> select(t record); } |
接口和方法都使用了泛型,使用該通用方法的接口需要指定泛型的類型。通過 java 反射可以很容易得到接口泛型的類型信息,代碼如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
type[] types = mapperclass.getgenericinterfaces(); class <?> entityclass = null ; for (type type : types) { if (type instanceof parameterizedtype) { parameterizedtype t = (parameterizedtype) type; //判斷父接口是否為 selectmapper.class if (t.getrawtype() == selectmapper. class ) { //得到泛型類型 entityclass = ( class <?>) t.getactualtypearguments()[ 0 ]; break ; } } } |
實體類中添加的 jpa 注解只是一種映射實體和數據庫表關系的手段,通過一些默認規則或者自定義注解也很容易設置這種關系,獲取實體和表的對應關系后,就可以根據通用接口方法定義的功能來生成和 xml 中一樣的 sql 代碼。動態生成 xml 樣式代碼的方式有很多,最簡單的方式就是純 java 代碼拼字符串,通用 mapper 為了盡可能的少的依賴選擇了這種方式。如果使用模板(如 freemarker,velocity 和 beetl 等模板引擎)實現,自由度會更高,也能方便開發人員調整。
在 mybatis 中,每一個方法(注解或 xml 方式)經過處理后,最終會構造成 mappedstatement 實例,這個對象包含了方法id(namespace+id)、結果映射、緩存配置、sqlsource 等信息,和 sql 關系最緊密的是其中的 sqlsource,mybatis 最終執行的 sql 時就是通過這個接口的 getboundsql 方法獲取的。
在 mybatis 中,使用@selectprovider 這種方式定義的方法,最終會構造成 providersqlsource,providersqlsource 是一種處于中間的 sqlsource,它本身不能作為最終執行時使用的 sqlsource,但是他會根據指定方法返回的 sql 去構造一個可用于最后執行的 staticsqlsource,staticsqlsource的特點就是靜態 sql,支持在 sql 中使用#{param} 方式的參數,但是不支持 <if>,<where> 等標簽。
為了能根據實體類動態生成支持動態 sql 的方法,通用 mapper 從這里入手,利用providersqlsource 可以生成正常的 mappedstatement,可以直接利用 mybatis 各種配置和命名空間的特點(這是通用 mapper 選擇這種方式的主要原因)。在生成 mappedstatement 后,“過河拆橋” 般的利用完就把 providersqlsource 替換掉了,正常情況下,providersqlsource 根本就沒有執行的機會。在通用 mapper 定義的實現方法中,提供了 mappedstatement 作為參數,有了這個參數,我們就可以根據 ms 的 id(規范情況下是 接口名.方法名)得到接口,通過接口的泛型可以獲取實體類(entityclass),根據實體和表的關系我們可以拼出 xml 方式的動態 sql,一個簡單的方法如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/** * 查詢全部結果 * * @param ms * @return */ public string selectall(mappedstatement ms) { final class <?> entityclass = getentityclass(ms); //修改返回值類型為實體類型 setresulttype(ms, entityclass); stringbuilder sql = new stringbuilder(); sql.append(sqlhelper.selectallcolumns(entityclass)); sql.append(sqlhelper.fromtable(entityclass, tablename(entityclass))); sql.append(sqlhelper.orderbydefault(entityclass)); return sql.tostring(); } |
拼出的 xml 形式的動態 sql,使用 mybatis 的 xmllanguagedriver 中的 createsqlsource 方法可以生成 sqlsource。然后使用反射用新的 sqlsource 替換providersqlsource 即可,如下代碼。
1
2
3
4
5
6
7
8
9
10
|
/** * 重新設置sqlsource * * @param ms * @param sqlsource */ protected void setsqlsource(mappedstatement ms, sqlsource sqlsource) { metaobject msobject = systemmetaobject.forobject(ms); msobject.setvalue( "sqlsource" , sqlsource); } |
metaobject 是mybatis 中很有用的工具類,mybatis 的結果映射就是靠這種方式實現的。反射信息使用的 defaultreflectorfactory,這個類會緩存反射信息,因此 mybatis 的結果映射的效率很高。
到這里核心的內容都已經說完了,雖然知道怎么去替換 sqlsource了,但是!什么時候去替換呢?
這一直都是一個難題,如果不大量重寫 mybatis 的代碼很難萬無一失的完成這個任務。通用 mapper 并沒有去大量重寫,主要是考慮到以后的升級,也因此在某些特殊情況下,通用 mapper 的方法會在沒有被替換的情況下被調用,這個問題在將來的 mybatis 3.5.x 版本中會以更友好的方式解決(目前的 providersqlsource 已經比以前能實現更多的東西,后面會講)。
針對不同的運行環境,需要用不同的方式去替換。當使用純 mybatis (沒有spring)方式運行時,替換很簡單,因為會在系統中初始化 sqlsessionfactory,可以初始化的時候進行替換,這個時候也不會出現前面提到的問題。替換的方式也很簡單,通過 sqlsessionfactory 可以得到 sqlsession,然后就能得到 configuration,通過 configuration.getmappedstatements() 就能得到所有的 mappedstatement,循環判斷其中的方法是否為通用接口提供的方法,如果是就按照前面的方式替換就可以了。
在使用 spring 的情況下,以繼承的方式重寫了 mapperscannerconfigurer 和 mapperfactorybean,在 spring 調用 checkdaoconfig 的時候對 sqlsource 進行替換。在使用 spring boot 時,提供的 mapper-starter 中,直接注入 list<sqlsessionfactory> sqlsessionfactorylist 進行替換。
下面我們按照這個思路,以最簡練的代碼,實現一個通用方法。
實現一個簡單的通用 mapper
1. 定義通用接口方法
1
2
3
4
|
public interface basemapper<t> { @selectprovider (type = selectmethodprovider. class , method = "select" ) list<t> select(t entity); } |
這里定義了一個簡單的 select 方法,這個方法判斷參數中的屬性是否為空,不為空的字段會作為查詢條件進行查詢,下面是對應的 provider。
1
2
3
4
5
|
public class selectmethodprovider { public string select(object params) { return "什么都不是!" ; } } |
這里的 provider 不會最終執行,只是為了在初始化時可以生成對應的 mappedstatement。
2. 替換 sqlsource
下面代碼為了簡單,都指定的 basemapper 接口,并且沒有特別的校驗。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
public class simplemapperhelper { public static final xmllanguagedriver xml_language_driver = new xmllanguagedriver(); /** * 獲取泛型類型 */ public static class getentityclass( class <?> mapperclass){ type[] types = mapperclass.getgenericinterfaces(); class <?> entityclass = null ; for (type type : types) { if (type instanceof parameterizedtype) { parameterizedtype t = (parameterizedtype) type; //判斷父接口是否為 basemapper.class if (t.getrawtype() == basemapper. class ) { //得到泛型類型 entityclass = ( class <?>) t.getactualtypearguments()[ 0 ]; break ; } } } return entityclass; } /** * 替換 sqlsource */ public static void changems(mappedstatement ms) throws exception { string msid = ms.getid(); //標準msid為 包名.接口名.方法名 int lastindex = msid.lastindexof( "." ); string methodname = msid.substring(lastindex + 1 ); string interfacename = msid.substring( 0 , lastindex); class <?> mapperclass = class .forname(interfacename); //判斷是否繼承了通用接口 if (basemapper. class .isassignablefrom(mapperclass)){ //判斷當前方法是否為通用 select 方法 if (methodname.equals( "select" )) { class entityclass = getentityclass(mapperclass); //必須使用<script>標簽包裹代碼 stringbuffer sqlbuilder = new stringbuffer( "<script>" ); //簡單使用類名作為包名 sqlbuilder.append( "select * from " ).append(entityclass.getsimplename()); field[] fields = entityclass.getdeclaredfields(); sqlbuilder.append( " <where> " ); for (field field : fields) { sqlbuilder.append( "<if test=\"" ) .append(field.getname()).append( "!=null\">" ); //字段名直接作為列名 sqlbuilder.append( " and " ).append(field.getname()) .append( " = #{" ).append(field.getname()).append( "}" ); sqlbuilder.append( "</if>" ); } sqlbuilder.append( "</where>" ); sqlbuilder.append( "</script>" ); //解析 sqlsource sqlsource sqlsource = xml_language_driver.createsqlsource( ms.getconfiguration(), sqlbuilder.tostring(), entityclass); //替換 metaobject msobject = systemmetaobject.forobject(ms); msobject.setvalue( "sqlsource" , sqlsource); } } } } |
changems 方法簡單的從 msid 開始,獲取接口和實體信息,通過反射回去字段信息,使用 <if> 標簽動態判斷屬性值,這里的寫法和 xml 中一樣,使用 xmllanguagedriver 處理時需要在外面包上 <script> 標簽。生成 sqlsource 后,通過反射替換了原值。
3. 測試
針對上面代碼,提供一個 country 表和對應的各種類。
實體類。
1
2
3
4
5
6
|
public class country { private long id; private string countryname; private string countrycode; //省略 getter,setter } |
mapper 接口。
1
2
3
|
public interface countrymapper extends basemapper<country> { } |
啟動 mybatis 的公共類。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class sqlsessionhelper { private static sqlsessionfactory sqlsessionfactory; static { try { reader reader = resources.getresourceasreader( "mybatis-config.xml" ); sqlsessionfactory = new sqlsessionfactorybuilder().build(reader); reader.close(); //創建數據庫 sqlsession session = null ; try { session = sqlsessionfactory.opensession(); connection conn = session.getconnection(); reader = resources.getresourceasreader( "hsqldb.sql" ); scriptrunner runner = new scriptrunner(conn); runner.setlogwriter( null ); runner.runscript(reader); reader.close(); } finally { if (session != null ) { session.close(); } } } catch (ioexception ignore) { ignore.printstacktrace(); } } public static sqlsession getsqlsession() { return sqlsessionfactory.opensession(); } } |
配置文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?xml version= "1.0" encoding= "utf-8" ?> <!doctype configuration public "-//mybatis.org//dtd config 3.0//en" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration> <environments default = "development" > <environment id= "development" > <transactionmanager type= "jdbc" > <property name= "" value= "" /> </transactionmanager> <datasource type= "unpooled" > <property name= "driver" value= "org.hsqldb.jdbcdriver" /> <property name= "url" value= "jdbc:hsqldb:mem:basetest" /> <property name= "username" value= "sa" /> </datasource> </environment> </environments> <mappers> < package name= "tk.mybatis.simple.mapper" /> </mappers> </configuration> |
初始化sql。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
drop table country if exists; create table country ( id integer, countryname varchar( 32 ), countrycode varchar( 2 ) ); insert into country (id, countryname, countrycode) values( 1 , 'angola' , 'ao' ); insert into country (id, countryname, countrycode) values( 23 , 'botswana' , 'bw' ); -- 省略部分 insert into country (id, countryname, countrycode) values( 34 , 'chile' , 'cl' ); insert into country (id, countryname, countrycode) values( 35 , 'china' , 'cn' ); insert into country (id, countryname, countrycode) values( 36 , 'colombia' , 'co' ); |
測試代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class simpletest { public static void main(string[] args) throws exception { sqlsession sqlsession = sqlsessionhelper.getsqlsession(); configuration configuration = sqlsession.getconfiguration(); hashset<mappedstatement> mappedstatements = new hashset<mappedstatement>(configuration.getmappedstatements()); //如果注釋下面替換步驟就會出錯 for (mappedstatement ms : mappedstatements) { simplemapperhelper.changems(ms); } //替換后執行該方法 countrymapper mapper = sqlsession.getmapper(countrymapper. class ); country query = new country(); //可以修改條件或者注釋條件查詢全部 query.setcountrycode( "cn" ); list<country> countrylist = mapper.select(query); for (country country : countrylist) { system.out.printf( "%s - %s\n" , country.getcountryname(), country.getcountrycode()); } sqlsession.close(); } } |
通過簡化版的處理過程應該可以和前面的內容聯系起來,從而理解通用 mapper 的簡單處理過程。
完整代碼下載:simple-mapper
最新的 providersqlsource
早期的 providersqlsource 有個缺點就是定義的方法要么沒有參數,要么只能是 object parameterobject 參數,這個參數最終的形式在開發時也不容易一次寫對,因為不同形式的接口的參數會被 mybatis 處理成不同的形式,可以參考 深入了解mybatis參數。由于沒有提供接口和類型相關的參數,因此無法根據類型實現通用的方法。
在最新的 3.4.5 版本中,providersqlsource 增加了一個額外可選的 providercontext 參數,這個類如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
/** * the context object for sql provider method. * * @author kazuki shimizu * @since 3.4.5 */ public final class providercontext { private final class <?> mappertype; private final method mappermethod; /** * constructor. * * @param mappertype a mapper interface type that specified provider * @param mappermethod a mapper method that specified provider */ providercontext( class <?> mappertype, method mappermethod) { this .mappertype = mappertype; this .mappermethod = mappermethod; } /** * get a mapper interface type that specified provider. * * @return a mapper interface type that specified provider */ public class <?> getmappertype() { return mappertype; } /** * get a mapper method that specified provider. * * @return a mapper method that specified provider */ public method getmappermethod() { return mappermethod; } } |
有了這個參數后,就能獲取到接口和當前執行的方法信息,因此我們已經可以實現通用方法了。
下面是一個官方測試中的簡單例子,定義的通用接口如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public interface basemapper<t> { @selectprovider (type= oursqlbuilder. class , method= "buildselectbyidprovidercontextonly" ) @containslogicaldelete t selectbyid(integer id); @retention (retentionpolicy.runtime) @target (elementtype.method) @interface containslogicaldelete { boolean value() default false ; } @retention (retentionpolicy.runtime) @target (elementtype.type) @interface meta { string tablename(); } } |
接口定義了一個簡單的根據 id 查詢的方法,定義了一個邏輯刪除的注解、還有一個表名的元注解。
下面是 方法的實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public string buildselectbyidprovidercontextonly(providercontext context) { //獲取方法上的邏輯刪除注解 final boolean containslogicaldelete = context.getmappermethod(). getannotation(basemapper.containslogicaldelete. class ) != null ; //獲取接口上的元注解(不是實體) final string tablename = context.getmappertype(). getannotation(basemapper.meta. class ).tablename(); return new sql(){{ select( "*" ); from(tablename); where( "id = #{id}" ); if (!containslogicaldelete){ where( "logical_delete = ${constants.logical_delete_off}" ); } }}.tostring(); } |
這里相比之前,可以獲取到更多的信息,sql 也不只是固定表的查詢,可以根據 @meta 注解制定方法查詢的表名,和原來一樣的是,最終還是返回一個簡單的 sql 字符串,仍然不支持動態 sql 的標簽。
下面是實現的接口。
1
2
3
4
|
@basemapper .meta(tablename = "users" ) public interface mapper extends basemapper<user> { } |
上面實現的方法中,注解從接口獲取的,因此這里也是在 mapper 上配置的 meta 接口。
按照前面通用 mapper 中的介紹,在實現方法中是可以獲取 user 類型的,因此如果把注解定義在實體類上也是可行的。
現在看起來已經很不錯了,但是還不支持動態 sql,還不能緩存根據 sql 生成的 sqlsource,因此每次執行都需要執行方法去生成 sqlsource,仍然還有改進的地方,為了解決這個問題,我提交了兩個pr #1111,#1120,目前還在討論階段,真正實現可能要到 3.5.0 版本。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/isea533/article/details/78493852