好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

MyBatis通用Mapper实现原理及相关内容

mybatis通用mapper实现原理

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

/**

  * 查询全部结果

  * @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

/**

  * 重新设置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

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

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

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

<?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

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

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 的简单处理过程。

最新的 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

/**

  * 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

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

@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实现原理及相关内容的详细内容...

  阅读:14次