mabatis的原理

本文会先介绍通用 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"
< 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_jb51.rar

最新的 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 版本。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

                    <div class="art_xg">
                    <h4>您可能感兴趣的文章:</h4><ul><li><a href="/article/157618.htm" title="Mybatis MapperScannerConfigurer自动扫描Mapper接口生成代理注入到Spring的方法" target="_blank">Mybatis MapperScannerConfigurer自动扫描Mapper接口生成代理注入到Spring的方法</a></li><li><a href="/article/156329.htm" title="MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式" target="_blank">MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式</a></li><li><a href="/article/153427.htm" title="MyBatis直接执行SQL的工具SqlMapper" target="_blank">MyBatis直接执行SQL的工具SqlMapper</a></li><li><a href="/article/153405.htm" title="MyBatis通用Mapper实现原理及相关内容" target="_blank">MyBatis通用Mapper实现原理及相关内容</a></li><li><a href="/article/153375.htm" title="Java通用Mapper UUID简单示例" target="_blank">Java通用Mapper UUID简单示例</a></li><li><a href="/article/152768.htm" title="详解MyBatis开发Dao层的两种方式(Mapper动态代理方式)" target="_blank">详解MyBatis开发Dao层的两种方式(Mapper动态代理方式)</a></li><li><a href="/article/146129.htm" title="Spring Boot集成MyBatis实现通用Mapper的配置及使用" target="_blank">Spring Boot集成MyBatis实现通用Mapper的配置及使用</a></li><li><a href="/article/141343.htm" title="详解Mybatis通用Mapper介绍与使用" target="_blank">详解Mybatis通用Mapper介绍与使用</a></li><li><a href="/article/139659.htm" title="Spring Boot整合mybatis并自动生成mapper和实体实例解析" target="_blank">Spring Boot整合mybatis并自动生成mapper和实体实例解析</a></li><li><a href="/article/157877.htm" title="Mapper批量插入Oracle数据@InsertProvider注解" target="_blank">Mapper批量插入Oracle数据@InsertProvider注解</a></li></ul>
                    </div>
					<div class="lbd_bot clearfix">
					<div id="_79b1zzr3dc" style="width: 100%;"><ins style="width:0px;height:0px;"></ins><iframe width="820" frameborder="0" height="250" scrolling="no" src="https://pos.baidu.com/s?wid=820&amp;hei=250&amp;di=u4846790&amp;ltu=https%3A%2F%2Fwww.jb51.net%2Farticle%2F149170.htm&amp;psi=834051c3ae5b25e13df234dc60e93bcd&amp;dc=3&amp;ti=%E6%B5%85%E8%B0%88MyBatis%E9%80%9A%E7%94%A8Mapper%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86_java_%E8%84%9A%E6%9C%AC%E4%B9%8B%E5%AE%B6&amp;ps=10023x260&amp;drs=1&amp;pcs=1691x818&amp;pss=1691x11897&amp;cfv=0&amp;cpl=3&amp;chi=1&amp;cce=true&amp;cec=GBK&amp;tlm=1585577429&amp;psr=1707x960&amp;par=1707x920&amp;pis=-1x-1&amp;ccd=24&amp;cja=false&amp;cmi=4&amp;col=zh-CN&amp;cdo=-1&amp;tcn=1585577430&amp;dtm=HTML_POST&amp;tpr=1585577429825&amp;ari=2&amp;ant=0&amp;exps=111000,118013,110011&amp;prot=2&amp;dis=0&amp;dai=1&amp;dri=0&amp;ltr=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DXjkmlIDOcpLMCabbprG_NDWz1yRUhsiyXrNh6H7SciyCD8Dtf8SkPoC0Vj7FX7UiGjzOdufRPgiZPMNLYyvqXa%26wd%3D%26eqid%3Ded11669f00077b99000000065e81fdc2"></iframe></div><script type="text/javascript" src="//jscode.jbzj.com/production/ql/common/h/source/n/h/production/kmtr.js"></script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值