本篇文章給大家分享的是有關MyBatis中Mapper的實現(xiàn)原理是什么,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
創(chuàng)新互聯(lián)建站專注于平昌網(wǎng)站建設服務及定制,我們擁有豐富的企業(yè)做網(wǎng)站經驗。 熱誠為您提供平昌營銷型網(wǎng)站建設,平昌網(wǎng)站制作、平昌網(wǎng)頁設計、平昌網(wǎng)站官網(wǎng)定制、微信平臺小程序開發(fā)服務,打造平昌網(wǎng)絡公司原創(chuàng)品牌,更為您提供平昌網(wǎng)站排名全網(wǎng)營銷落地服務。
基本原理
通用 Mapper 提供了一些通用的方法,這些通用方法是以接口的形式提供的,例如。
public interface SelectMapper{ /** * 根據(jù)實體中的屬性值進行查詢,查詢條件使用等號 */ @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL") List select(T record); }
接口和方法都使用了泛型,使用該通用方法的接口需要指定泛型的類型。通過 Java 反射可以很容易得到接口泛型的類型信息,代碼如下。
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 注解只是一種映射實體和數(shù)據(jù)庫表關系的手段,通過一些默認規(guī)則或者自定義注解也很容易設置這種關系,獲取實體和表的對應關系后,就可以根據(jù)通用接口方法定義的功能來生成和 XML 中一樣的 SQL 代碼。動態(tài)生成 XML 樣式代碼的方式有很多,最簡單的方式就是純 Java 代碼拼字符串,通用 Mapper 為了盡可能的少的依賴選擇了這種方式。如果使用模板(如 FreeMarker,Velocity 和 beetl 等模板引擎)實現(xiàn),自由度會更高,也能方便開發(fā)人員調整。
在 MyBatis 中,每一個方法(注解或 XML 方式)經過處理后,最終會構造成 MappedStatement 實例,這個對象包含了方法id(namespace+id)、結果映射、緩存配置、SqlSource 等信息,和 SQL 關系最緊密的是其中的 SqlSource,MyBatis 最終執(zhí)行的 SQL 時就是通過這個接口的 getBoundSql 方法獲取的。
在 MyBatis 中,使用@SelectProvider 這種方式定義的方法,最終會構造成 ProviderSqlSource,ProviderSqlSource 是一種處于中間的 SqlSource,它本身不能作為最終執(zhí)行時使用的 SqlSource,但是他會根據(jù)指定方法返回的 SQL 去構造一個可用于最后執(zhí)行的 StaticSqlSource,StaticSqlSource的特點就是靜態(tài) SQL,支持在 SQL 中使用#{param} 方式的參數(shù),但是不支持
為了能根據(jù)實體類動態(tài)生成支持動態(tài) SQL 的方法,通用 Mapper 從這里入手,利用ProviderSqlSource 可以生成正常的 MappedStatement,可以直接利用 MyBatis 各種配置和命名空間的特點(這是通用 Mapper 選擇這種方式的主要原因)。在生成 MappedStatement 后,“過河拆橋” 般的利用完就把 ProviderSqlSource 替換掉了,正常情況下,ProviderSqlSource 根本就沒有執(zhí)行的機會。在通用 Mapper 定義的實現(xiàn)方法中,提供了 MappedStatement 作為參數(shù),有了這個參數(shù),我們就可以根據(jù) ms 的 id(規(guī)范情況下是 接口名.方法名)得到接口,通過接口的泛型可以獲取實體類(entityClass),根據(jù)實體和表的關系我們可以拼出 XML 方式的動態(tài) SQL,一個簡單的方法如下。
/** * 查詢全部結果 * * @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 形式的動態(tài) SQL,使用 MyBatis 的 XMLLanguageDriver 中的 createSqlSource 方法可以生成 SqlSource。然后使用反射用新的 SqlSource 替換ProviderSqlSource 即可,如下代碼。
/** * 重新設置SqlSource * * @param ms * @param sqlSource */ protected void setSqlSource(MappedStatement ms, SqlSource sqlSource) { MetaObject msObject = SystemMetaObject.forObject(ms); msObject.setValue("sqlSource", sqlSource); }
MetaObject 是MyBatis 中很有用的工具類,MyBatis 的結果映射就是靠這種方式實現(xiàn)的。反射信息使用的 DefaultReflectorFactory,這個類會緩存反射信息,因此 MyBatis 的結果映射的效率很高。
到這里核心的內容都已經說完了,雖然知道怎么去替換 SqlSource了,但是!什么時候去替換呢?
這一直都是一個難題,如果不大量重寫 MyBatis 的代碼很難萬無一失的完成這個任務。通用 Mapper 并沒有去大量重寫,主要是考慮到以后的升級,也因此在某些特殊情況下,通用 Mapper 的方法會在沒有被替換的情況下被調用,這個問題在將來的 MyBatis 3.5.x 版本中會以更友好的方式解決(目前的 ProviderSqlSource 已經比以前能實現(xiàn)更多的東西,后面會講)。
針對不同的運行環(huán)境,需要用不同的方式去替換。當使用純 MyBatis (沒有Spring)方式運行時,替換很簡單,因為會在系統(tǒng)中初始化 SqlSessionFactory,可以初始化的時候進行替換,這個時候也不會出現(xiàn)前面提到的問題。替換的方式也很簡單,通過 SqlSessionFactory 可以得到 SqlSession,然后就能得到 Configuration,通過 configuration.getMappedStatements() 就能得到所有的 MappedStatement,循環(huán)判斷其中的方法是否為通用接口提供的方法,如果是就按照前面的方式替換就可以了。
在使用 Spring 的情況下,以繼承的方式重寫了 MapperScannerConfigurer 和 MapperFactoryBean,在 Spring 調用 checkDaoConfig 的時候對 SqlSource 進行替換。在使用 Spring Boot 時,提供的 mapper-starter 中,直接注入 List
下面我們按照這個思路,以最簡練的代碼,實現(xiàn)一個通用方法。
實現(xiàn)一個簡單的通用 Mapper
1. 定義通用接口方法
public interface BaseMapper{ @SelectProvider(type = SelectMethodProvider.class, method = "select") List select(T entity); }
這里定義了一個簡單的 select 方法,這個方法判斷參數(shù)中的屬性是否為空,不為空的字段會作為查詢條件進行查詢,下面是對應的 Provider。
public class SelectMethodProvider { public String select(Object params) { return "什么都不是!"; } }
這里的 Provider 不會最終執(zhí)行,只是為了在初始化時可以生成對應的 MappedStatement。
2. 替換 SqlSource
下面代碼為了簡單,都指定的 BaseMapper 接口,并且沒有特別的校驗。
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); //必須使用"); //解析 sqlSource SqlSource sqlSource = XML_LANGUAGE_DRIVER.createSqlSource( ms.getConfiguration(), sqlBuilder.toString(), entityClass); //替換 MetaObject msObject = SystemMetaObject.forObject(ms); msObject.setValue("sqlSource", sqlSource); } } } }
changeMs 方法簡單的從 msId 開始,獲取接口和實體信息,通過反射回去字段信息,使用