MyBatis通用Mapper實(shí)現(xiàn)原理及相關(guān)內(nèi)容
MyBatis通用Mapper實(shí)現(xiàn)原理
本文會(huì)先介紹通用 Mapper 的簡單原理,然后使用最簡單的代碼來實(shí)現(xiàn)這個(gè)過程。
基本原理
通用 Mapper 提供了一些通用的方法,這些通用方法是以接口的形式提供的,例如。
public interface SelectMapper<T> {
/**
* 根據(jù)實(shí)體中的屬性值進(jìn)行查詢,查詢條件使用等號(hào)
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> 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;
}
}
}
實(shí)體類中添加的 JPA 注解只是一種映射實(shí)體和數(shù)據(jù)庫表關(guān)系的手段,通過一些默認(rèn)規(guī)則或者自定義注解也很容易設(shè)置這種關(guān)系,獲取實(shí)體和表的對(duì)應(yīng)關(guān)系后,就可以根據(jù)通用接口方法定義的功能來生成和 XML 中一樣的 SQL 代碼。動(dòng)態(tài)生成 XML 樣式代碼的方式有很多,最簡單的方式就是純 Java 代碼拼字符串,通用 Mapper 為了盡可能的少的依賴選擇了這種方式。如果使用模板(如FreeMarker,Velocity 和 beetl 等模板引擎)實(shí)現(xiàn),自由度會(huì)更高,也能方便開發(fā)人員調(diào)整。
在 MyBatis 中,每一個(gè)方法(注解或 XML 方式)經(jīng)過處理后,最終會(huì)構(gòu)造成 MappedStatement 實(shí)例,這個(gè)對(duì)象包含了方法id(namespace+id)、結(jié)果映射、緩存配置、SqlSource 等信息,和 SQL 關(guān)系最緊密的是其中的 SqlSource,MyBatis 最終執(zhí)行的 SQL 時(shí)就是通過這個(gè)接口的 getBoundSql 方法獲取的。
在 MyBatis 中,使用@SelectProvider 這種方式定義的方法,最終會(huì)構(gòu)造成 ProviderSqlSource,ProviderSqlSource 是一種處于中間的 SqlSource,它本身不能作為最終執(zhí)行時(shí)使用的 SqlSource,但是他會(huì)根據(jù)指定方法返回的 SQL 去構(gòu)造一個(gè)可用于最后執(zhí)行的 StaticSqlSource,StaticSqlSource的特點(diǎn)就是靜態(tài) SQL,支持在 SQL 中使用#{param} 方式的參數(shù),但是不支持 <if>,<where> 等標(biāo)簽。
為了能根據(jù)實(shí)體類動(dòng)態(tài)生成支持動(dòng)態(tài) SQL 的方法,通用 Mapper 從這里入手,利用ProviderSqlSource 可以生成正常的 MappedStatement,可以直接利用 MyBatis 各種配置和命名空間的特點(diǎn)(這是通用 Mapper 選擇這種方式的主要原因)。在生成 MappedStatement 后,“過河拆橋” 般的利用完就把 ProviderSqlSource 替換掉了,正常情況下,ProviderSqlSource 根本就沒有執(zhí)行的機(jī)會(huì)。在通用 Mapper 定義的實(shí)現(xiàn)方法中,提供了 MappedStatement 作為參數(shù),有了這個(gè)參數(shù),我們就可以根據(jù) ms 的 id(規(guī)范情況下是 接口名.方法名)得到接口,通過接口的泛型可以獲取實(shí)體類(entityClass),根據(jù)實(shí)體和表的關(guān)系我們可以拼出 XML 方式的動(dòng)態(tài) SQL,一個(gè)簡單的方法如下。
/**
* 查詢?nèi)拷Y(jié)果
* @param ms
* @return
*/
public String selectAll(MappedStatement ms) {
final Class<?> entityClass = getEntityClass(ms);
//修改返回值類型為實(shí)體類型
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 形式的動(dòng)態(tài) SQL,使用 MyBatis 的 XMLLanguageDriver 中的 createSqlSource 方法可以生成 SqlSource。然后使用反射用新的 SqlSource 替換ProviderSqlSource 即可,如下代碼。
/**
* 重新設(shè)置SqlSource
* @param ms
* @param sqlSource
*/
protected void setSqlSource(MappedStatement ms, SqlSource sqlSource) {
MetaObject msObject = SystemMetaObject.forObject(ms);
msObject.setValue("sqlSource", sqlSource);
}
MetaObject 是MyBatis 中很有用的工具類,MyBatis 的結(jié)果映射就是靠這種方式實(shí)現(xiàn)的。反射信息使用的 DefaultReflectorFactory,這個(gè)類會(huì)緩存反射信息,因此 MyBatis 的結(jié)果映射的效率很高。
到這里核心的內(nèi)容都已經(jīng)說完了,雖然知道怎么去替換 SqlSource了,但是!什么時(shí)候去替換呢?
這一直都是一個(gè)難題,如果不大量重寫 MyBatis 的代碼很難萬無一失的完成這個(gè)任務(wù)。通用 Mapper 并沒有去大量重寫,主要是考慮到以后的升級(jí),也因此在某些特殊情況下,通用 Mapper 的方法會(huì)在沒有被替換的情況下被調(diào)用,這個(gè)問題在將來的 MyBatis 3.5.x 版本中會(huì)以更友好的方式解決(目前的 ProviderSqlSource 已經(jīng)比以前能實(shí)現(xiàn)更多的東西,后面會(huì)講)。
針對(duì)不同的運(yùn)行環(huán)境,需要用不同的方式去替換。當(dāng)使用純 MyBatis (沒有Spring)方式運(yùn)行時(shí),替換很簡單,因?yàn)闀?huì)在系統(tǒng)中初始化 SqlSessionFactory,可以初始化的時(shí)候進(jìn)行替換,這個(gè)時(shí)候也不會(huì)出現(xiàn)前面提到的問題。替換的方式也很簡單,通過 SqlSessionFactory 可以得到 SqlSession,然后就能得到 Configuration,通過 configuration.getMappedStatements() 就能得到所有的 MappedStatement,循環(huán)判斷其中的方法是否為通用接口提供的方法,如果是就按照前面的方式替換就可以了。
在使用 Spring 的情況下,以繼承的方式重寫了 MapperScannerConfigurer 和 MapperFactoryBean,在 Spring 調(diào)用 checkDaoConfig 的時(shí)候?qū)?SqlSource 進(jìn)行替換。在使用 Spring Boot 時(shí),提供的 mapper-starter 中,直接注入 List<SqlSessionFactory> sqlSessionFactoryList 進(jìn)行替換。
下面我們按照這個(gè)思路,以最簡練的代碼,實(shí)現(xiàn)一個(gè)通用方法。
實(shí)現(xiàn)一個(gè)簡單的通用Mapper
1. 定義通用接口方法
public interface BaseMapper<T> {
@SelectProvider(type = SelectMethodProvider.class, method = "select")
List<T> select(T entity);
}
這里定義了一個(gè)簡單的 select 方法,這個(gè)方法判斷參數(shù)中的屬性是否為空,不為空的字段會(huì)作為查詢條件進(jìn)行查詢,下面是對(duì)應(yīng)的 Provider。
public class SelectMethodProvider {
public String select(Object params) {
return "什么都不是!";
}
}
這里的 Provider 不會(huì)最終執(zhí)行,只是為了在初始化時(shí)可以生成對(duì)應(yīng)的 MappedStatement。
2. 替換 SqlSource
下面代碼為了簡單,都指定的 BaseMapper 接口,并且沒有特別的校驗(yàn)。
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();
//標(biāo)準(zhǔn)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)){
//判斷當(dāng)前方法是否為通用 select 方法
if (methodName.equals("select")) {
Class entityClass = getEntityClass(mapperClass);
//必須使用<script>標(biāo)簽包裹代碼
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 開始,獲取接口和實(shí)體信息,通過反射回去字段信息,使用 <if> 標(biāo)簽動(dòng)態(tài)判斷屬性值,這里的寫法和 XML 中一樣,使用 XMLLanguageDriver 處理時(shí)需要在外面包上 <script> 標(biāo)簽。生成 SqlSource 后,通過反射替換了原值。
3. 測(cè)試
針對(duì)上面代碼,提供一個(gè) country 表和對(duì)應(yīng)的各種類。
實(shí)體類。
public class Country {
private Long id;
private String countryname;
private String countrycode;
//省略 getter,setter
}
Mapper 接口。
public interface CountryMapper extends BaseMapper<Country> {
}
啟動(dòng) MyBatis 的公共類。
public class SqlSessionHelper {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
//創(chuàng)建數(shù)據(jù)庫
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();
}
}
配置文件。
<?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。
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');
測(cè)試代碼。
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());
//如果注釋下面替換步驟就會(huì)出錯(cuò)
for (MappedStatement ms : mappedStatements) {
SimpleMapperHelper.changeMs(ms);
}
//替換后執(zhí)行該方法
CountryMapper mapper = sqlSession.getMapper(CountryMapper.class);
Country query = new Country();
//可以修改條件或者注釋條件查詢?nèi)?
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();
}
}
通過簡化版的處理過程應(yīng)該可以和前面的內(nèi)容聯(lián)系起來,從而理解通用 Mapper 的簡單處理過程。
最新的 ProviderSqlSource
早期的 ProviderSqlSource 有個(gè)缺點(diǎn)就是定義的方法要么沒有參數(shù),要么只能是 Object parameterObject 參數(shù),這個(gè)參數(shù)最終的形式在開發(fā)時(shí)也不容易一次寫對(duì),因?yàn)椴煌问降慕涌诘膮?shù)會(huì)被 MyBatis 處理成不同的形式,可以參考深入了解MyBatis參數(shù)。由于沒有提供接口和類型相關(guān)的參數(shù),因此無法根據(jù)類型實(shí)現(xiàn)通用的方法。
在最新的 3.4.5 版本中,ProviderSqlSource 增加了一個(gè)額外可選的 ProviderContext 參數(shù),這個(gè)類如下。
/**
* 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;
}
}
有了這個(gè)參數(shù)后,就能獲取到接口和當(dāng)前執(zhí)行的方法信息,因此我們已經(jīng)可以實(shí)現(xiàn)通用方法了。
下面是一個(gè)官方測(cè)試中的簡單例子,定義的通用接口如下。
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();
}
}
接口定義了一個(gè)簡單的根據(jù) id 查詢的方法,定義了一個(gè)邏輯刪除的注解、還有一個(gè)表名的元注解。
下面是 方法的實(shí)現(xiàn)。
public String buildSelectByIdProviderContextOnly(ProviderContext context) {
//獲取方法上的邏輯刪除注解
final boolean containsLogicalDelete = context.getMapperMethod().
getAnnotation(BaseMapper.ContainsLogicalDelete.class) != null;
//獲取接口上的元注解(不是實(shí)體)
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 也不只是固定表的查詢,可以根據(jù) @Meta 注解制定方法查詢的表名,和原來一樣的是,最終還是返回一個(gè)簡單的 SQL 字符串,仍然不支持動(dòng)態(tài) SQL 的標(biāo)簽。
下面是實(shí)現(xiàn)的接口。
@BaseMapper.Meta(tableName = "users")
public interface Mapper extends BaseMapper<User> {
}
上面實(shí)現(xiàn)的方法中,注解從接口獲取的,因此這里也是在 Mapper 上配置的 Meta 接口。
按照前面通用 Mapper 中的介紹,在實(shí)現(xiàn)方法中是可以獲取 User 類型的,因此如果把注解定義在實(shí)體類上也是可行的。
現(xiàn)在看起來已經(jīng)很不錯(cuò)了,但是還不支持動(dòng)態(tài) SQL,還不能緩存根據(jù) SQL 生成的 SqlSource,因此每次執(zhí)行都需要執(zhí)行方法去生成 SqlSource,仍然還有改進(jìn)的地方,為了解決這個(gè)問題,我提交了兩個(gè) PR #1111,#1120,目前還在討論階段,真正實(shí)現(xiàn)可能要到 3.5.0 版本。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請(qǐng)查看下面相關(guān)鏈接
相關(guān)文章
Mybatis攔截器實(shí)現(xiàn)公共字段填充的示例代碼
本文介紹了使用Spring Boot和MyBatis實(shí)現(xiàn)公共字段的自動(dòng)填充功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12
詳解Spring的autowire-candidate設(shè)計(jì)
現(xiàn)在的Spring應(yīng)用通常都是基于注解開發(fā),但是對(duì)Spring感興趣的同學(xué)可以借助Spring早期基于Xml配置的各種運(yùn)用來加深對(duì)Spring框架內(nèi)部的理解和體會(huì)Spring框架的設(shè)計(jì)之妙。這篇文章我們就來談?wù)刋ml配置之default-autowire-candidates2021-06-06
完美解決Eclipse 項(xiàng)目有紅感嘆號(hào)的問題
下面小編就為大家?guī)硪黄昝澜鉀QEclipse 項(xiàng)目有紅感嘆號(hào)的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-01-01
使用@Autowired注解引入server服務(wù)層方法時(shí)報(bào)錯(cuò)的解決
這篇文章主要介紹了使用@Autowired注解引入server服務(wù)層方法時(shí)報(bào)錯(cuò)的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
詳解MyBatis Generator自動(dòng)創(chuàng)建代碼(dao,mapping,poji)
這篇文章主要介紹了詳解MyBatis Generator自動(dòng)創(chuàng)建代碼(dao,mapping,poji)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10
springmvc處理模型數(shù)據(jù)Map過程解析
這篇文章主要介紹了springmvc處理模型數(shù)據(jù)Map過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
servlet之web路徑問題_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了servlet之web路徑問題的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07

