浅谈MyBatis通用Mapper实现原理

作者:isea533 时间:2022-11-18 18:45:16 

本文会先介绍通用 Mapper 的简单原理,然后使用最简单的代码来实现这个过程。

基本原理

通用 Mapper 提供了一些通用的方法,这些通用方法是以接口的形式提供的,例如。


public interface SelectMapper<T> {
 /**
  * 根据实体中的属性值进行查询,查询条件使用等号
  */
 @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;
   }
 }
}

实体类中添加的 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,一个简单的方法如下。


/**
* 查询全部结果
*
* @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 即可,如下代码。


/**
* 重新设置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. 定义通用接口方法


public interface BaseMapper<T> {
 @SelectProvider(type = SelectMethodProvider.class, method = "select")
 List<T> select(T entity);
}

这里定义了一个简单的 select 方法,这个方法判断参数中的属性是否为空,不为空的字段会作为查询条件进行查询,下面是对应的 Provider。


public class SelectMethodProvider {
 public String select(Object params) {
   return "什么都不是!";
 }
}

这里的 Provider 不会最终执行,只是为了在初始化时可以生成对应的 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);
       //必须使用<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 表和对应的各种类。

实体类。


public class Country {
private Long  id;
private String countryname;
private String countrycode;
//省略 getter,setter
}

Mapper 接口。


public interface CountryMapper extends BaseMapper<Country> {

}

启动 MyBatis 的公共类。


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();
 }

}

 配置文件。


<?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');

测试代码。


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_jb51.rar

最新的 ProviderSqlSource

早期的 ProviderSqlSource 有个缺点就是定义的方法要么没有参数,要么只能是 Object parameterObject 参数,这个参数最终的形式在开发时也不容易一次写对,因为不同形式的接口的参数会被 MyBatis 处理成不同的形式,可以参考 深入了解MyBatis参数。由于没有提供接口和类型相关的参数,因此无法根据类型实现通用的方法。

在最新的 3.4.5 版本中,ProviderSqlSource 增加了一个额外可选的 ProviderContext 参数,这个类如下。


/**
* 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;
}

}

有了这个参数后,就能获取到接口和当前执行的方法信息,因此我们已经可以实现通用方法了。

下面是一个官方测试中的简单例子,定义的通用接口如下。


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 查询的方法,定义了一个逻辑删除的注解、还有一个表名的元注解。

下面是 方法的实现。


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 的标签。

下面是实现的接口。


@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

标签:MyBatis,通用,Mapper
0
投稿

猜你喜欢

  • unity实现翻页按钮功能

    2021-07-15 03:33:47
  • springboot更新配置Swagger3的一些小技巧

    2023-08-28 06:31:43
  • java异常处理执行顺序详解try catch finally

    2022-10-01 04:10:10
  • 详解SpringCloud Config配置中心

    2021-06-18 04:38:34
  • SpringCloud @RefreshScope刷新机制浅析

    2022-12-13 11:38:15
  • Java输入年份和月份判断多少天实例代码

    2023-12-23 10:43:11
  • 三行Android代码实现白天夜间模式流畅切换

    2021-06-11 08:15:12
  • Java创建树形结构算法实例代码

    2021-11-21 22:24:01
  • C#基于DBContext(EF)实现通用增删改查的REST方法实例

    2021-06-23 20:56:07
  • 浅谈利用Spring的AbstractRoutingDataSource解决多数据源的问题

    2021-09-07 07:20:59
  • c# 可选参数、命名参数

    2022-08-06 05:38:41
  • 分享java中设置代理的两种方式

    2023-10-28 10:48:52
  • Spring boot如何集成kaptcha并生成验证码

    2023-09-13 04:00:24
  • 一行命令同时修改maven项目中多个module的版本号的方法

    2023-09-13 09:43:10
  • c#动态执行脚本的3种方式详解

    2021-09-11 13:41:58
  • C#实现顺序栈和链栈的代码实例

    2021-08-17 02:36:40
  • .net 随机生成汉字

    2022-01-22 08:33:33
  • C#使用NPOI导入Excel的方法详解

    2021-08-29 07:39:35
  • js事件模型与自定义事件实例解析

    2021-05-26 06:15:06
  • Android 用 camera2 API 自定义相机

    2023-04-30 11:29:37
  • asp之家 软件编程 m.aspxhome.com