tk-mybatis使用

  • 一、序言
  • 二、组件介绍
    • 1、前置条件
    • 2、独立性和兼容性
    • 3、环境的搭建
      • ⑴、独立环境的使用
        • ①自定义mybatis-config.xml配置文件
        • ②创建测试表以及数据
        • ③构建SqlSessionFactory
        • ④创建PO对象以及Mapper接口
        • ⑤配置和测试Mapper对象
        • ⑥小结
      • ⑵、基于Spring环境(不依赖于与mybatis-spring组件)
        • ①构建SqlSessionFactory
        • ②创建测试表以及数据
        • ③创建PO对象以及Mapper接口
        • ④配置Mapper对象(方式一)
        • ⑤配置Mapper对象(方式二)
        • ⑥测试Mapper对象
        • ⑦小结
      • ⑶、基于Spring环境(兼容mybatis-spring组件)
      • ⑷、基于现有架构体系下研发环境的搭建(最优)
      • ⑸、基于现有架构体系下研发环境的搭建(兼容)
  • 三、组件的使用
    • 0301、基础API介绍
      • 030101、基本函数的使用列表
    • 0302、高级功能介绍
      • 030201、高级API使用介绍
      • 030202、实体对象配置
      • 030203、字段属性配置
      • 030204、主键属性配置
      • 030205、条件表达式的使用
      • 030206、字段值生成器的使用
      • 030207、批量更新的使用
      • 030208、超长SQL语句检查
      • 030209、超大记录结果返回检查
      • 030210、错误日志详细化
      • 030211、聚合函数
      • 030212、autoMapper
      • 030213、全局配置
    • 0303、生态扩展介绍
      • 030301、原生态Plugin技术之Interceptor
      • 030302、值生成规则之GeneratedGenId
      • 030303、值生成规则之GeneratedKeyValue
      • 030304、扩展机制之MapperProvider
      • 030305、扩展机制之HackInterceptor

 

一、序言


本文主要目的是介绍平台组件之Mapper组件的使用方法,使得一位研发工程师在本文的帮助下就能够达到运用熟练的基本目的。同时还介绍了Mapper组件的生态能力,为后续广大使用者使用和扩展Mapper组件起到奠基的作用。

本组件最大的优点是生态扩展能力,自带的基础功能仅为提供最基本的组件特征,只有强大的生态能力,才能让该组件得到日益的丰富,而不是单靠一个人,一个组件就能带来一片生机。

如果你想迅速了解Mapper组件带来了什么,请直接查看第三章。

二、组件介绍


1、前置条件

学习和使用Mapper组件的前置条件:

①、软件版本-基于Mybatis3.0.5,以及mybatis-spring-1.0.1,配套的Spring版本为3.0.5;

②、编译JDK版本号:1.6,支持运行环境1.7,未使用到1.7+的特性代码;

③、掌握基本的Mybatis3.0+,以及Spring3.0+的使用方法;

④、额外依赖:slf4j-api 任意版本(不传递依赖),

2、独立性和兼容性

☆、Mapper组件没有修改Mybatis原生态的代码,而是采用扩展和改写的方式来扩展和增强Mybatis的使用;

☆、Mapper组件可以独立使用,可以在任意能够使用Mybatis 3.0+的系统中直接使用;

☆、Mapper组件采用的是反射,继承,可选组件兼容等方式来增强Mybatis的能力,因此升级Mybatis组件可能会存在版本不兼容的情况;

☆、Mapper组件采用Maven管理,对外部组件依赖采用provided,也即不传递依赖任何其它组件,如果需要,自行导入;

☆、Mapper组件对在spring环境下,对mybatis-spring-1.0.1版本的依赖是可选的,即便我们使用到的Spring组件;

☆、Mapper组件对在spring环境下,对Spring组件的依赖是可选的,如果需要使用到Spring,那么建议版本为 [3.0.5, 4.0.0);

☆、Mapper组件对于已有的使用Mybatis的项目(可以是Spring环境下)是能够兼容的(稍许核心配置需要修改);

 

 

3、环境的搭建

POM依赖如下:

expand source

其中framework.mapper.version值,为引用的版本号,请参见发行手册。以下使用都是基于本章节的前置条件的基础之上的。

注:因该组件采用的scope为provider进行组件的依赖,因此不会做任何传递依赖,需要自行引入Mybatis组件。

⑴、独立环境的使用


①自定义mybatis-config.xml配置文件

注:内容不做额外的限制,并不依赖于sqlmap配置文件

②创建测试表以及数据

注:测试数据采用的是hsqldb的语法,如果使用oracle数据库,请自行调整。

③构建SqlSessionFactory

注:其中getConfigFileAsReader() 为①的配置文件的流对象。

④创建PO对象以及Mapper接口

注:类名和表名一致,除了首字符大写,命名规则有限制,不要使用驼峰模式(后续有介绍原因);

注:字段名和②列名一致,命名规则有限制,不要使用驼峰模式(后续有介绍原因);

注:此处省略了构造函数以及GET和SET方法,请自行补充;

注:此Mapper的类名命名规则无限制,仅需要继承Mapper组件中的通用Mapper接口即可,其中T为泛型为上述的实体类对象(必须)。

⑤配置和测试Mapper对象

注:使用Mapper前,需要将待测试的Mapper接口,添加进去(添加的方式有多种,这里目前只演示这一种)。

注:没有任何要求,跟原生态一样

⑥小结

使用方面,除了构造SqlSessionFactory跟原生态不一致(更换了一个API)其它基本相同。

注:更多使用,请参考第4章节(组件的使用)

⑵、基于Spring环境(不依赖于与mybatis-spring组件)


①构建SqlSessionFactory

注:需要配置MapperSqlSessionFactoryBean,且仅依赖一个数据源,其它所有的配置均为可选,你没看错,可选的mybatis-config.xml,可选的sqlmap.xml。

②创建测试表以及数据

参见上一章节

③创建PO对象以及Mapper接口

参见上一章节

④配置Mapper对象(方式一)

注:常规的执行配置MapperFactoryBean,并且不依赖于mybatis-spring(Mapper组件已经提供),仅需要配置上述的SqlSessionFactory即可。

⑤配置Mapper对象(方式二)

注:采用配置应用端Mapper接口的包路径的扫描方式即可,完成所有该目录下的Mapper的配置能力,减少大面积的配置,省心省事。

注:使用端可以采用ByType方式注入,或者采用ByType方式从Spring容器中获取。又或者使用默认的Spring的Name生成规则,采用ID,本例的CountryMapper的ID应该为(countryMapper)。

⑥测试Mapper对象

注:直接获取,或者依赖注入。

⑦小结

除了构造SqlSessionFactory的FactoryBean类更换以外,其它基本相同。

注:更多使用,请参考第4章节(组件的使用)

⑶、基于Spring环境(兼容mybatis-spring组件)

本章节除去配置Mapper对象(方式一)不一致外,其它所有跟上小章节一致。

注:区别在于一个使用原生态mybatis-spring,一个使用了Mapper组件自带的MapperFactoryBean。目前可以做到无缝兼容共存,省去一个外部依赖包。

⑷、基于现有架构体系下研发环境的搭建(最优)

保持和(2)章节一致,其中配置部分采用扫描方式即可。

⑸、基于现有架构体系下研发环境的搭建(兼容)

保持和现有模式一致,仅需要替换掉net.carefx.framework.mybatis.FcSqlSessionFactoryBean为 com.cenboomh.hit.framework.mybatis.spring.MapperSqlSessionFactoryBean即可。

其中有关  是可选配置,为了保持兼容极度压缩减少切换成本,Mapper组件提供一个空的set方法。

 

 

三、组件的使用

本章节主要介绍Mapper组件的基本用法,高级用法,以及生态扩展能力。

 

0301、基础API介绍

在仅拥有一个普通的JavaBean,一个空的Mapper接口(继承通用Mapper),没有任何额外的配置文件的情况下能给我们带来什么呢?

我们先使用IDEA开发工具,在拿到CountryMapper对象的句柄的前提下(参见环境搭建章节),看看该对象能够提供什么?如下图:

乍一眼看下去,竟然拥有多达20+个函数可供我们使用。但是远远不止,Mapper组件提供生态的扩展能力,可以使得广大研发工程师可以自行扩展,并无缝加入到Mapper组件中,以维持该组件的再生能力(详情参见“生态扩展”章节)。

先不说这20+个函数到底如何使用,有什么功效,关键是我们研发人员仅仅就写了一个普通的javaBean,一个空的接口,就这样了?是的,没错就是怎么简单,这么任性。

030101、基本函数的使用列表

我们分别来介绍下目前版本中的13个函数都能起到什么作用(其余部分在高级使用中介绍),以及使用上需要注意事项吧。

序号

函数声明

函数入参

函数出参

函数作用

使用说明

测试代码截图

测试日志截图

1 selectAll List 查询表中所有记录,并返回对象的集合 该方法一般用于小表的全表查询,不适用于数据量达到或者超过K级别的情况
2 select T List

根据入参的条件,进行绝对匹配筛选,

并返回对象的集合

该方法如果入参为null,则和selectAll的效果是一致的;

该方法如果入参对象存在,但是其属性都是null,则和selectAll的效果是一致的;

 

3

selectByRowBounds

T、

RowBounds

List

根据入参的条件进行分页,

进行绝对匹配筛选,并返回对象的集合

该方法与select一样,只是重载了一个带RowBounds的参数(不允许为null);

因Mapper本身不支持方法重名,因此就更换了函数名;

如果外部分页查询是拦截RowBounds参数的,可以直接无缝对接;

4 selectOne T T

根据入参的条件,进行绝对匹配筛选,

并返回对象

该方法如果查询到0-1个结果,则返回null或者

如果该方法的条件查询到多个,则抛出TooManyResultsException错误;

5 selectCount T int

根据入参的条件,进行绝对匹配筛选,

并返回符合条件的结果总数

该方法如果入参为null,则返回的是表的记录总数;

条件采用的是绝对匹配;

6 selectByPrimaryKey Object T 根据主键对象,查询该主键所表示的对象

如果是单主键,那么直接传递主键对象即可,例如long、String等;

如果是复合主键,可以传递对象,对象的复合主键都存在值;

或者传递Map,其中key为复合主键的key,也是可以的;

复合主键情况用的较少;

7 existsWithPrimaryKey Object boolean 根据主键对象,判断该主键所表示的记录是否存在 和selectByPrimaryKey函数作用一致,仅采用 0,1 结合rs.getBoolean来做返回值
8 insert T int 根据对象T的属性,生成插入语句,并返回影响的行数

NULL字段也会插入进行;

 但是需要指定该字段的jdbcType,否则抛出错误(Oracle是个奇葩,其它好多数据库都可以,不抛错);

如何指定字段的jdbcType,参见高级用法;

如果T对象中有字段不想插入,可以配置注解insertable=false;

如果T对象中有字段不想插入,可以配置注解Transient(该字段不参与自动生成语句);

9 insertSelective T int 根据对象T的属性,生成插入语句,并返回影响的行数

NULL字段不会被插入进去,因此对象T的属性不要使用8大简单类型;

需要使用其包装类,我们实际环境运用中常常是该函数(空属性不插入)

其余特性和insert相同;

10 delete T int

根据对象T的参数,进行绝对匹配,并删除该记录

返回删除的行数

该方法如果入参为null,则删除的是表的所有数;

该方法如果入参为new T(),则删除的是表的所有数;

条件采用的是绝对匹配;

如果配置参数safeDelete没有指定,是不允许空条件删除,会抛错(参加高级参数配置部分);

11 deleteByPrimaryKey Object int

根据主键对象,进行删除该主键所表述的记录,

并返回删除的行数

如果是单主键,那么直接传递主键对象即可,例如long、String等;

如果是复合主键,可以传递对象,对象的复合主键都存在值;

或者传递Map,其中key为复合主键的key,也是可以的;

复合主键情况用的较少;

12 updateByPrimaryKey T int 根据对象T的参数的主键数据,进行更新该记录

主键数据必须存在;

如果T对象中的属性为NULL,也会更新到数据库中,但需要设置jdbcType;

(Oracle是个奇葩,其它好多数据库都可以,不抛错);

如何指定字段的jdbcType,参见高级用法;

如果T对象中有字段不想更新,可以配置注解updateable=false;

如果T对象中有字段不想更新,可以配置注解Transient(该字段不参与自动生成语句);

13 updateByPrimaryKeySelective T int 根据对象T的参数的主键数据,进行更新该记录

主键数据必须存在;

如果T对象中的属性为NULL,不会更新到数据库中;

如果T对象中有字段不想更新,可以配置注解updateable=false;

如果T对象中有字段不想更新,可以配置注解Transient(该字段不参与自动生成语句);

需要使用其包装类,我们实际环境运用中常常是该函数(空属性不更新)

0302、高级功能介绍

本章节主要介绍下Mapper组件的高级部分的使用,同时介绍哪些功能是可以在老代码上使用,哪些是在Mapper组件特定情况下使用的。

有关高级生态扩展部分,请参见 03.Mapper组件的使用-生态扩展

030201、高级API使用介绍

本小结主要介绍剩余15个高级API的使用,当然不仅仅只有这个,因为这是一个生态,会陆陆续续提炼更多的便捷的API。

序号

函数声明

函数入参

函数出参

函数作用

使用说明

测试代码截图

测试日志截图

1 insertList List int[] 根据对象T的属性,执行批量插入语句,并返回影响的行数

该方法和基础API中的insert一致;

采用了SQL自带的batch功能来完成批量动作,无需使用者额外分批;

该方法不会产生超长SQL语句;

该方法支持自动生成字段值,详见:030206自动生成器的使用;

   
2 insertSelectiveList List int[]

根据对象T的属性,执行批量插入语句,并返回影响的行数

该方法和基础API中的insertSelective一致;

采用了SQL自带的batch功能来完成批量动作,无需使用者额外分批;

该方法不会产生超长SQL语句;

该方法支持自动生成字段值,详见:030206自动生成器的使用;

我们常常使用该方法完成批量插入,解决性能、超长SQL问题;

 

 

3 deleteList List int[]

根据对象T的参数,进行绝对匹配,并批量删除,

返回删除的行数

该方法和基础API中的delete一致;

采用了SQL自带的batch功能来完成批量动作,无需使用者额外分批;

该方法不会产生超长SQL语句;

   
4 deleteByPrimaryKeyList List int[]

根据主键对象作为入参,并批量删除,返回删除的行数

该方法和基础API中的deleteByPrimaryKey一致;

采用了SQL自带的batch功能来完成批量动作,无需使用者额外分批;

该方法不会产生超长SQL语句;

   
5 updateByPrimaryKeyList List int[] 根据对象T的参数的主键数据,进行批量更新

该方法和基础API中的updateByPrimaryKey一致;

采用了SQL自带的batch功能来完成批量动作,无需使用者额外分批;

该方法不会产生超长SQL语句;

   
6 updateByPrimaryKeySelectiveList List int[] 根据对象T的参数的主键数据,进行批量更新

该方法和基础API中的updateByPrimaryKeySelective一致;

采用了SQL自带的batch功能来完成批量动作,无需使用者额外分批;

该方法不会产生超长SQL语句;

我们常常使用该方法完成批量更新,解决性能、超长SQL问题;

   
7 selectByCondition Condition List 根据Condition条件对象,进行查询

该方法属于基础API中的select增强版本

Condition的使用和构造详见:条件表达式的使用;

该方法支持锁定也即forUpdate;

   
8 selectByConditionAndRowBounds

Condition、

RowBounds

List 根据Condition条件对象,进行分页查询

该方法和selectByCondition一样;

仅为分页查询重载了一个RowBounds参数;

   
9 selectCountByCondition Condition int 根据Condition条件对象,进行求符合条件的行数

该方法属于基础API中的selectCount增强版本

Condition的使用和构造详见:条件表达式的使用;

该方法支持锁定也即forUpdate;

   
10 selectOneByCondition Condition T 根据Condition条件对象,进行求符合条件的单对象

该方法属于基础API中的selectOne增强版本

Condition的使用和构造详见:条件表达式的使用;

该方法支持锁定也即forUpdate;

   
11 deleteByCondition Condition int 根据Condition条件对象,进行删除记录行

该方法属于基础API中的delete增强版本

Condition的使用和构造详见:条件表达式的使用;

   
12 updateByCondition

T、

Condition

int

根据Condition条件对象,对符合条件的数据,

进行按照T属性更新

该方法增强基础API的更新方法;

该方法针对T中所有的属性都强制更新,包括NULL;

Condition的使用和构造详见:条件表达式的使用;

   
13 updateByConditionSelective

T、

Condition

int

根据Condition条件对象,对符合条件的数据,

进行按照T属性更新

该方法属于基础API中的delete增强版本

该方法针对T中所有的属性不为NULL的都更新;

Condition的使用和构造详见:条件表达式的使用;

   
14 selectAggregationByCondition

Condition、

AggregateCondition

List

根据Condition条件对象、AggregateCondition聚合对象,

对符合条件的数据,按照要求进行分组聚合查询

该方法用于聚合查询

Condition的使用和构造详见:条件表达式的使用;

   
15 selectOneAggregationByCondition

Condition、

AggregateCondition

S

根据Condition条件对象、AggregateCondition聚合对象,

对符合条件的数据,按照要求进行分组聚合查询,

保证结果只有单个结果,不封装直接返回该结果

该方法用于聚合查询

返回的泛型值类型取决于查询的结果类型

Condition的使用和构造详见:条件表达式的使用;

   

030202、实体对象配置

当开发者使用Mapper注解时候,对于实体的定义是有一定约束的。

① 命名约束

Mapper组件内置的5种命名转换风格:原值、驼峰转下划线、转换为大写、转换为小写、驼峰转下划线大写形式。

注解为:@com.cenboomh.hit.framework.mybatis.mapper.code.Style

该注解使用在实体类上,可以不指定,默认值为:驼峰转下划线

情况说明:

原值:顾名思义,什么都不动,实体类名 Country转换为Country,BillItem转换为BillItem;

驼峰转下划线:实体类名 Country转换为country,BillItem转换为bill_item;

转换为大写:实体类名 Country转换为COUNTRY,BillItem转换为BILLITEM;

转换为小写:实体类名 Country转换为country,BillItem转换为billitem;

驼峰转下划线大写形式:实体类名 Country转换为COUNTRY,BillItem转换为BILL_ITEM;

对于Oracle数据库而言,表名与字段名不区分大小写,因此,常用的就是:原值、驼峰转下划线。

②表名的自定义

对于Mapper组件自带的风格转换不满足实际需要时候,我们可以指定实体类上的注解。

注解为:@com.cenboomh.hit.framework.mybatis.mapper.annotation.Table

该注解使用在实体类上,还可以指定额外的schema和catalog,不同数据库使用的不一样。

使用原则是:如果指定了值,例如[catalog].tablename 表格对象的优先级 catalog > schema > 全局的catalog > 全局的schema。

全局的schema和catalog使用配置参数schema和catalog来设置,默认为不指定。指定其中一个以后。

表格的名称在SQL语句中则会变成  catalog.tablename 的形式。

030203、字段属性配置

当开发者使用Mapper组件的时候,对于实体的字段定义也是有一定的约束的。

① 字段类型要求

实体类中的字段类型要求:

★不能为8大基本类型,因基本类型一定会存在默认值,不会为null的情况,因此在很多使用场景下造成歧义,那么就禁用它,因此该字段类型会被忽略;

★不能为复杂对象类型,因复杂对象类型存在则无法和数据库字段类型进行对应的问题,因此该字段也会被忽略;

②自定义忽略字段

当我们的实体类中因某些特殊原因,需要定义一个与数据库表字段无关的字段时候,可以采用配置注解来忽略它,以免自动生成的SQL语句中带有错误的字段。

注解为:@com.cenboomh.hit.framework.mybatis.mapper.annotation.Transient

③字段名命名要求

实体类中的字段命名要求:

★如果不额外指定字段的特殊配置的情况下,字段命名的会根据表格的命名风格进行转换,转换规则见实体对象配置

★如果发现因特殊原因导致Mapper组件内置的规则不足以满足实际需要时候,可以指定字段注解:@com.cenboomh.hit.framework.mybatis.mapper.annotation.ColumnType;

其中ColumnType注解可以指定:列名(数据库真实表名)、是否运用于插入语句(默认允许)、是否运用于更新语句(默认允许),同时还可以指定jdbcType(默认不指定)。

④配置默认的查询排序字段

当我们使用基本API进行常规查询后,我们需要对返回List的函数,进行排序,则可以定义默认的排序字段注解。

注解:@com.cenboomh.hit.framework.mybatis.mapper.annotation.OrderBy

可以自行指定ASC/DESC,默认为ASC。

⑤如果查询排序的默认规则不满足需求怎么办?

可以使用Condition作为查询条件的函数来指定自定义的排序。

030204、主键属性配置

当开发者使用Mapper组件时候,想使用实体主键的相关API,例如函数selectByPrimaryKey。那么主键的定义和识别非常重要。

要想Mapper组件能够自动识别主键,需要按照Mapper组件的约束条件来定义组件

① 默认主键

当定义一个实体时候,如果该实体的所有字段配置中并没有指定主键字段时候,且配置参数useDefaultId=true默认为true。

会再次尝试查找以一个字段属性为id的列作为默认主键,这里是以属性为id,而不是列为id。

如果属性为id的也不存在,则会将所有配置字段列为复合主键。

②自定义主键

可以通过在某个配置字段上通过增加注解标记,将其标记为主键,那么默认主键的策略是不会生效的。

注解为:@com.cenboomh.hit.framework.mybatis.mapper.annotation.Id

允许有多个配置字段为主键,那么在使用有主键参与的函数时候,需要额外注意使用规则。

 

警告:如果指定的@Id为注解字段,是简单类型,符合对象,以及存在@Transient,同样会被忽略。

030205、条件表达式的使用

条件表达式主要用于针对基础API中采用绝对匹配不满足复杂的需求而生的。

可用方法:deleteByCondition、selectAggregationByCondition、selectByCondition、selectByConditionAndRowBounds、selectCountByCondition、selectOneAggregationByCondition、selectOneByCondition、updateByCondition、updateByConditionSelective

使用方法:

★实例化Condition对象,传入操作表对应的类。

★使用select方法指定要查询的属性列,exclude方法排除查询字段。当两者都设定时,取select的设定;两者都不设定时,查询类对应的所有字段。

★orderBy、asc/desc指定结果排序,可以指定多个字段排序。

★distinct指定结果是否去重。

★forUpdate指定语句是否加锁。

★where/orWhere/andWhere指定条件的起始部分,后面可连接各种查询条件,每个起始部分和下一个起始部分之间的语句会被括号包括,可以实现类似(a || b) && (c || d)的功能。

★查询条件中in语句传入的list会自动根据全局inSplitSize参数拆分参数,可以突破oracle in参数不能超过1000的限制。

030206、字段值生成器的使用


当开发者想在新增数据时候,部分字段能够自动生成,并回填到数据对象中,这是不是很能简化我们的实际代码的编写的复杂性呢?

对于很多初学者,习惯使用数据库自带的sequence,不同的数据库的使用方式略有差别。但是这个问题在生成自增的字段时候问题不大(运维备份,迁移时候可能就存在很多挑战了)。

当我们生成特殊的数据类型,比如说:结算号、挂号单号,还有测试时候使用的随机码等等。

① GeneratedGenId的使用

使用特性:Mapper组件内置一个最简单的模块用于在新增数据时候,能够按照要求自动填充值。

使用场景:Mapper组件自带的insert、insertList、insertSelective、insertSelectiveList能够识别,其它函数暂时无法识别。历史代码不支持,在批量执行的情况下,仍然是一条条的填充值。

使用条件:在上述4个函数所使用的实体对象中,需要自动填充的字段上增加@com.cenboomh.hit.framework.mybatis.mapper.annotation.GeneratedGenId注解

详情说明:

★简单回顾:本特性仅用于Mapper组件自带的4个插入函数,不适用于其它。

★先看GeneratedGenId的声明

该注解用于实体对象的字段上,表明该字段在插入时候需要生成自动填充,填充规则,则是ID生成器 com.cenboomh.hit.framework.mybatis.mapper.genid.GenId 接口的实现类。

该注解默认的实现类为GenId的UUID实现,也即根据JDK自带的UUID算法生成一个字符串36位(中级带有4位-号)。也即该实现仅能用于字符串字段上,不能用于其它类型字段。

再看GenId的声明

该接口传入表格名称以及列名称,供ID生成器算法来识别和使用。默认提供两个实现,一个是UUID(36位字符串),一个是UUID去掉-号(32位字符串)。

该接口属于生态扩展一部分,使用者可以执行制定规则,然后见其实现类用于注解的value上可以。

该接口的实例采用懒加载机制实例化,属于单例模型。该类的实例无法自动释放,完全交由虚拟机来管理。

测试代码及日志截图

非常之简单,我们可以自定义GenId的实现(可以定义非String类型的),来替换默认的UUID。

② GeneratedKeyValue的使用

使用特性:Mapper组件内置一个可高度扩展的数据生成器的插件生态模型,以便支撑各种数据填充的能力,支撑批量。GeneratedGenId搞不定,就找我。

使用场景:Mapper组件自带的insert、insertList、insertSelective、insertSelectiveList能够识别,其它函数暂时不识别。历史代码可以支撑。

使用条件:在上述4个函数、以及使用者自定义的Mapper上的函数填上注解@com.cenboomh.hit.framework.mybatis.mapper.annotation.GeneratorKeyProvider即可识别在插入动作(使用者自定义的函数必须是insert类型,其它类型会报错)。在自定义注解上增加@com.cenboomh.hit.framework.mybatis.mapper.annotation.GeneratedKeyValue注解,并在满足添加的函数所使用的实体对象中,需要自动填充的字段上增加自定义注解。

详情说明:详见生态扩展HackInterceptor之数据填充,本章节不做描述。

030207、批量更新的使用


当开发者使用了Mapper组件以后,就非常容易的可以使用批量更新的能力,不论新功能还是历史功能。

有关Mapper组件自带的批量新增、删除、更新详见:030201高级API的使用部分。

Q:本章节介绍的批量更新有啥特别的?难道Mybatis以前的批量不行吗?

A:是的,Mybatis自带的批量更新存在很多问题。

例如,

★我们可以insert into values 的方法,使用上和Mapper组件自带的批量一样简单,但是它会导致SQL语句非常非常长(取决于数据量的大小),带来的问题详见:030208、超长SQL语句检查中生产问题;

★又或者我们可以采用程序foreach,批量调用单个更新方法,但是这个在数据量达到一定程度的情况下造成性能问题;

★又或者我们可以利用Mybatis的sessionFactory.openSession (ExecutorType.BATCH)及其变种方式来构造批量执行句柄,然后foreach,批量调用单个更新方法(由于Mybatis在3.0的版本中该行为的批量操作在Spring容器存在严重的BUG。因此目前HIS和EMR都提供了一个封装改造的工具来使用,大多数业务都配套改造了,但是用起来不方便,导致不是每个角落都这样用的,要不然也不会存在:030208、超长SQL语句检查中生产问题了。)。

★当然,还有一种就是利用单条SQL,使用insert into... select from table的方法,由数据库中其它表里面的数据构造插入(目前也仅仅提倡大家这样使用,但是使用条件太苛刻了)。

综述:就是我们现有的体系中,要想批量操作(增、删、改)不够方便。

那么Mapper组件是怎么解决这一问题的呢?

答案是:简单,但有很容易达到的约束条件。

步骤:

★在一个需要批量操作的地方声明一个批量方法形参如:void addList(List billItems) ;

★在该方法上加上注解@com.cenboomh.hit.framework.mybatis.mapper.annotation.BatchProvider

★在sqlMap文件中,按照单个操作进行构造SQL语句即可;

★完毕

约束?对存在@com.cenboomh.hit.framework.mybatis.mapper.annotation.BatchProvider的方法,

☆要求返回值必须是void或者int[]不能是其它;

☆要求方法入参中执行批量的数据,必须是List或者[],其数据的位置可以是任意,不必要放在首位,可以通过BatchProvider中的paramIndex来指定,默认为0,也即首位;

☆完毕

这也叫约束?是的,如果正常的一个批量操作,这些约束是最基本的,跟Mapper组件的批量约束并无特殊关系。

如此简单?是的。

Mapper组件自带的insertList、insertSelectiveList、deleteList、deleteByPrimaryKeyList、updateByPrimaryKeyList、updateByPrimaryKeySelectiveList 这八个API已经采用了@BatchProvider.

那我们的历史代码是否能够支撑呢?是的,可以的。 看测试代码及其日志截图:

①mybatis-config.xml

②编写sqlmap

③编写Mapper

④编写测试代码

⑤执行并检查日志

⑥完毕

综述:Mapper组件自带强大的批量更新能力,利用了jdbc的batch能力,同时修复了batch不输出SQL日志的缺陷。

使用起来非常方便,即使在历史代码上做到最低要求,即可拥有强大的批量执行能力,你还不来使用?

原理:基本原理跟HIS和EMR解决批量操作的技术方式一样,只是见批量的工具封装到使用无法感知的位置,这个位置则是下一章节要重点说的生态扩展之HackInterceptor.

030208、超长SQL语句检查


当开发者使用了Mapper组件以后,已经默认自带了执行SQL语句的超长检查能力,不论是增删改查包括存储过程等哪种执行语句都会进行检查。

检查时机:Mapper组件的SQL语句长度检查是在Connection对象创建Statement时候,也即  StatementHandler.prepare(Connection connection) throws SQLException;  前。

检查机制:根据Mybatis的BoundSql.getSql() 获取执行的SQL语句字符串,例如:SELECT id,countryname,countrycode  FROM country  WHERE  id = ? AND countryname = ? 其中空格也算,这里的长度为82个字符。

生产案例: SDE-2848 - SDE质控模块sql绑定变量过多触发oracle bug导致数据库宕机 ( 已解决)  ,该事故中的SQL执行包括空格总长度达 3,418,211 字符(包括空格);

触发条件:Mapper组件默认的SQL语句长度为 1000个字符(目前没有发正式版本,待议),也即 (1000,+∞) 都会触发;

触发形态:Mapper组件默认只产生错误警告日志,可以通过配置checkSqlParameterLevel=dowarn/dothrow来配置是日志错误警告还是抛出异常,通过checkSqlParameterMaxLength=1000来设置检查长度;

日志警告实例(配置的是10个字符串长度测试使用):

com.cenboomh.hit.framework.mybatis.mapper.executor.sqlcheck.SuperLongSqlException: MapperStatementId=test.framework.mybatis.mapper.base.CountryMapper.selectOne,SQL语句实际长度[62],超过[10]警告:sql=SELECT id,countryname,countrycode  FROM country  WHERE  id = ?

其中异常对象或者错误信息中包括了几个关键性元素有:调用的SQL语句、调用的MapperStatementId、实际长度、以及配置的告警长度。

如何关闭:可以通过设置参数checkSqlParameterMaxLength的值为很大很大,以便关闭该检查。

性能影响:该检查功能的核心算法复杂度为O(1),纯内存比较算法,没有使用到反射,对原有程序性能没有影响;

改进机会:后期看生产情况,以及Oracle的检查机制,是否去掉必要空格,优化SQL语句长度算法。

030209、超大记录结果返回检查


当开发者使用了Mapper组件以后,已经默认自带了执行查询SQL语句的结果返回行数的检查能力。

检查时机:Mapper组件的查询结果返回行数检查是在ResultSet.next()时候,也即  FastResultSetHandler.shouldProcessMoreRows(ResultSet rs, ResultContext context, RowBounds rowBounds) throws SQLException  时候。

检查机制:根据已经返回的行数据(skip的不算数,也即没有参数结果数据对象,仅数据结果集.next()不算,这里一般指的是RowBounds内存分页的场景)和配置的警戒值进行比较。

生产案例:

触发条件:Mapper组件当结果集还有数据时候,并且当前的返回的实际行数已经等于配置的警戒值,则会触发;如果配置为错误告警日志模式,除了前面的条件外,还会在没有更多数据时候,判断是超过配置的警戒值,则会触发。

默认的返回结果数最大值为2000(目前没有正式发版,待议),也即(2000,+∞) 都会触发;

触发形态:Mapper组件默认只产生错误警告日志,可以通过配置checkSqlResultLevel=dowarn/dothrow来配置是日志错误警告还是抛出异常,通过checkSqlResultMaxSize=2000来设置允许返回的结果数最大值;

日志警告实例(配置的是50条测试使用):

com.cenboomh.hit.framework.mybatis.mapper.executor.morerowcheck.MoreRowException: MapperStatementId=test.framework.mybatis.mapper.base.CountryMapper.selectByConditionAndRowBounds,结果达到[50]警告:sql=SELECT  id,countryname,countrycode  FROM country

com.cenboomh.hit.framework.mybatis.mapper.executor.morerowcheck.MoreRowException: MapperStatementId=test.framework.mybatis.mapper.base.CountryMapper.selectByConditionAndRowBounds,结果数实际[51]超过[50]警告:sql=SELECT id,countryname,countrycode FROM country

配置的是警告的情况下,会在警戒值一条警告,最终值一条警告(如果系统没有宕机);配置的是抛异常的情况下,仅会在达到警戒值,并且还有更多结果时候抛出异常,并记录一次错误日志;

如何关闭:可以通过设置参数checkSqlResultMaxSize的值为很大很大,以便关闭该检查。

性能影响:该检查功能的核心算法复杂度为O(n),纯内存比较算法,没有使用到反射,对原有程序性能没有影响;

改进机会:暂无。

030210、错误日志详细化

当开发者使用了Mapper组件以后,如果在sql运行时发生了异常,会将该次处理中的所有信息,包括传递参数的详细内容进行打印,避免日志内容不明确定位问题困难。

030211、聚合函数

通过AggregateCondition对象,可以实现AVG、SUM、COUNT、MAX、MIN几个聚合函数。

可用方法:selectAggregationByCondition、selectOneAggregationByCondition

注意:

selectAggregationByCondition方法返回的结果list是聚合结果每一条数据,如果聚合结果字段在查询表class上没有,则需要另行定义结果类。

selectOneAggregationByCondition方法必须保证返回结果只有一行一列,返回结果的类型取决于查询结果类型,并非取决于泛型类型。

 

030212、autoMapper

autoMapper功能可以在不需要定义表专门的Mapper的情况下,实现自动生成Mapper接口

要使用autoMapper功能,在独立环境和spring下,需要配置不同的内容:

⑴、独立环境的使用

①向MapperConfiguration里注册表对应的实体类

注意使用addAutoMapper而不是addMapper

addAutoMapper

protected void configMapper(MapperConfiguration configuration) {

    configuration.addAutoMapper(Country.class);

}

②获取Mapper接口

获取方式和普通Mapper接口一样,区别在于传入实体类而不是Mapper接口,获取的对象需要手动转型为Mapper

getAutoMapper

SqlSession session = getSqlSessionFactory().openSession();

Mapper userMapper = (Mapper)session.getMapper(Country.class);

另一种方式,可以通过自动Mapper处理器获取到实际生成的Mapper接口,再通过该接口获取对象

getAutoMapper2

SqlSession session = getSqlSessionFactory().openSession();

Mapper userMapper = (Mapper)session.getMapper(new DefaultAutoMapperProcessor().getAutoMapper(Country.class));

③使用Mapper接口

通过autoMapper方式获取的Mapper和普通Mapper一样使用,由于无法扩展该接口,因此只会包括com.cenboomh.hit.framework.mybatis.mapper.common.Mapper接口已实现的方法

use autoMapper

userMapper.selectAll();

⑵、基于Spring环境,手动配置MapperFactoryBean

①配置MapperFactoryBean

与普通MapperFactoryBean配置不同的是在tableClass上配置实体bean,不要配置mapperInterface否则会优先读取mapperInterface

MapperFactoryBean

"testAutoMapper" class="com.cenboomh.hit.framework.mybatis.spring.mapper.MapperFactoryBean">

    "sqlSessionFactory" ref="sqlSessionFactory"/>

    "tableClass" value="test.com.cenboomh.hit.framework.mybatis.spring.auto.Country"/>

② 获取Mapper接口并使用

通过spring aop注入或者手动获取Mapper即可使用,注意如果使用注解方式,由于无法在编译器获取接口类,所以无法通过@Autowired只能通过@Resource获取

aop

"mapper" ref="testAutoMapper"/>

@Resource

@Resource(name = "testAutoMapper")

private Mapper testAutoMapper;

application.getBean

Mapper testMapper = (Mapper)applicationContext.getBean("testAutoMapper");

⑶、基于Spring环境,自动扫描

①在实体类上添加注解@RegisterAutoMapper

只有具有@RegisterAutoMapper注解的实体类才会被autoMapper处理,其他的地方和普通实体类一样,没有特殊限制

@RegisterAutoMapper

@RegisterAutoMapper

public class Country {

②配置扫描器AutoMapperScannerConfigurer 

AutoMapperScannerConfigurer与MapperScannerConfigurer配置方式相同,区别在于前者拦截实体类,后者拦截接口

AutoMapperScannerConfigurer

class="com.cenboomh.hit.framework.mybatis.spring.mapper.AutoMapperScannerConfigurer">

    "sqlSessionFactory" ref="sqlSessionFactory"/>

    "basePackage" value="test.com.cenboomh.hit.framework.mybatis.spring.auto"/>

③获取Mapper接口并使用

spring默认会读取实体类的名称并把它作为Mapper接口的名称(首字母小写),可以和手动配置一样获取

aop

"mapper" ref="country"/>

@Resource

@Resource(name = "country")

private Mapper mapper;

applicationContext.getBean

Mapper mapper = (Mapper)applicationContext.getBean("country");

如果Mapper接口名称有冲突或者有其他需要,可以在@RegisterAutoMapper注解上定义beanName,spring可以通过重写的beanName获取Mapper对象

 

@RegisterAutoMapper

@RegisterAutoMapper(beanName = "rewriteBeanNameMapper")

public class Country {

@Resource

@Resource(name = "rewriteBeanNameMapper")

private Mapper mapper;

030213、全局配置

本小节列举出当前Mapper组件内置的所有全局配置表,以便快速查阅。

① 配置位置

★可以使用new MapperSqlSessionFactoryBuilder().build(reader, properties)的代码中传递进去(这个一般是在单元测试使用,实际生产基本不这么使用);

★存在mybatis-config.xml的情况,可以直接使用其properties节点即可;

★在Spring环境下,可以使用进行配置;

★配置冲突生效优先级:Spring>mybatis-config.xml>build properties。

②配置列表

序号

配置项

默认值

可选值

使用场景

使用说明

1

catalog

""  

实体对象配置

可选,例如[catalog].tablename 表格对象的优先级 catalog > schema > 全局的catalog > 全局的schema
2

schema

""   实体对象配置 可选,例如[schema].tablename 表格对象的优先级 catalog > schema > 全局的catalog > 全局的schema
3

notEmpty

false false、true 生成SQL语句时候是否判断字符串的""

如果是false,那么当字段类型为字符串时候,仅判断入参和null比较,而不和""比较。

countryname,

如果是true,那么当字段类型为字符串时候,判断入参和null比较,也和""比较

countryname,

4

checkConditionEntityClass

false false、true

使用Condition对象时候,是否

验证Condition的实体对象和

Mapper所表述的实体类为同一个类型

false,表述不需要验证;

true,表述如果两者类型不一致,则抛错。

一般原则上要求,两者实体需要一致,但是不一致也没有什么特别的影响,需要慎重使用不一致的情况

5

style

camelhump

原值:normal、

驼峰转下划线:camelhump、

转换为大写:uppercase、

转换为小写:lowercase、

驼峰转下划线大写形式:camelhumpAndUppercase

实体对象配置

该配置会更改默认的实体对象的命名转换,包括字段的。

请谨慎修改默认值。一个系统模块应该保持一种风格,不建议中途修改。

6

safeDelete

true false、true 在执行Mapper自带的删除数据API时候,判断是否允许无条件删除,也即全表删除

true,表示如果一个删除语句并没有条件,则会抛错,不允许执行。

false,表示不做限制

内置删除全表安全,生产环境不建议开放。

7

safeUpdate

true false、true 在执行Mapper自带的更新数据API时候,判断是否允许无条件更新,也即全表更新

true,表示如果一个更新语句并没有条件,则会抛错,不允许执行。

false,表示不做限制

内置更新全表安全,生产环境不建议开放。

8

useJavaType

false false、true 在生成Mybatis的sql语句片段时候是否带上javaType

false,不带,也即#{id};

true,带上,#{id, javaType=java.lang.Long}

如果带上,那么就不能是用隐式转换,也即

selectByPrimaryKey("123456"),如果主键是Long,则报错,也即无法执行隐式类型转换。

9

useDefaultId

true false、true 在实体类没有配置主键注解时候,是否启用默认id字段作为注解

true,启用,默认采用id作为主键;

false,关闭,如果不显式指定,则采用全字段作为符合主键;

10

checkSqlResultMaxSize

2000   针对查询语句,检查结果返回的数据的总数是否达到指定值

可以设置很大很大以便关闭该检查,不建议这么做

默认值,后续需要根据实际生产运作情况进行微调。

11

checkSqlResultLevel

dowarn dowarn、dothrow 如果查询语句的检查结果返回的数据的总数超过指定的动作

dowarn,生成error日志,第一次超过一条,最终全部获取完一条;

dothrow,一旦超过,则抛出异常信息,中断执行;

12 checkSqlParameterMaxLength 1000   针对执行的SQL语句,检查该SQL语句执行前的字符串长度是否达到指定值(包括空格)

可以设置很大很大以便关闭该检查,不建议这么做

默认值,后续需要根据实际生产运作情况进行微调。

13 checkSqlParameterLevel dowarn dowarn、dothrow 如果执行的SQL语句执行前的字符串长度超过指定长度后的动作

dowarn,生成error日志;

dothrow,一旦超过,则抛出异常信息,中断执行;

 

 

0303、生态扩展介绍

本小节介绍下Mapper组件的生态扩展涵盖了哪些。

030301、原生态Plugin技术之Interceptor


本Mapper组件并不破坏Mybatis框架原生态的插件技术,因此所有基于Mybatis原生态开发的插件可以无缝集成。

030302、值生成规则之GeneratedGenId


当我们使用Mapper组件提供的4个插入函数时候,可以在需要的自动生成值的字段上填上@GeneratedGenId注解。

同时可以指定自定义的GenId接口的实现,即可完成扩展能力。就这么简单,无需额外配置。

该扩展能力已经在 字段值生成器的使用 章节中讲述。

030303、值生成规则之GeneratedKeyValue


@GeneratedGenId无法满足需求时候,可以在自定义注解上使用@GeneratedKeyValue来达成我们的复杂处理情况。

@GeneratedKeyValue注解要求注解在一个自定义注解上,这什么意思呢?

其实这里原本设计也是需要提供一个实现类,然后由Mapper组件进行调用。但是考虑到仅一个实现类作为扩展参数不足以满足更复杂场景以及简单的使用,同时也会导致高耦合。

因此该扩展能力要求有四步:

① 自定义注解的定义与配置

首先@GeneratedKeyValue注解要求提供一个任意的注解,但是我们这里要求是一个能在字段(Field)上使用的注解,并且@GeneratedKeyValue作为其父注解。

例如:

当然该注解可以增加一个属性,看扩展的人员的实际用途了。

然后在一个实体对象的字段上进行配置:

上述配置表示,id字段在插入语句执行的时候,启用一个叫@KeyGeneratorSequenceNo值生成规则。

② 值生成自定义实现

当我们配置完字段后,我们需要实现值生成规则。因此我们需要实现系统能够识别的一个接口。

接口定义如下:com.cenboomh.hit.framework.mybatis.mapper.generatorkey.GeneratorKeyHandler

其中入参GeneratorContext的定义如下:

该入参GeneratorContext可以获取到实体对象定义的表、列、字段、Session操作对象,以及值生成的注解对象。

process方法的第二个入参,也即传入的待填充的数据对象,可以是批量(参见批量插入),如果是批量,则数据对象只有一个值的数组。

process方法的第三个入参,也即函数API中存在多个参数的时候的附加值,具体参见详细案例。

案例:一个利用我们框架组件中的ID生成器的实现,如下:

③ 实现与注解关联

配置也有了,实现也有了,我们将该配置和实现关联起来,以便达到解耦合的目的,并且使得Mapper组件能够识别。

我们在构造SqlSessionFactoryBean时候来指定关联,如果非Spring容器,可以参见MapperConfiguration#addGeneratorKeyHandler方法,Spring容器就不用关注这个细节了。

④ 值生成规则的识别

当完成上述步骤后,我们需要在调用某些API函数(插入类型的)时候,值生成能够生效,因此需要在API函数上增加一个识别标志(不能是所有的函数都生效)。

配置生效的注解为:@com.cenboomh.hit.framework.mybatis.mapper.annotation.GeneratorKeyProvider  

API截图: 

★paramIndex主要用于需要自动生成的字段在API函数的位置,一般都是默认第一个位置,也即索引为0。

★entityClass主要用于配置值生成的规则实体类,也即存在有自定义注解(被@GeneratedKeyValue注解)的实体类,第一步中的要求。默认是不需要配置的,不配置会采用Mapper上的T的实体类的规则进行处理。但是为了使得没有使用的Mapper组件的代码也具备该能力的同时,不破坏原有代码(达到最大的可复用价值),则可以独立配置一个规则实体类(该规则类可以拥有仅仅值生成注解的字段,而不需要其它字段,因为它不参与实际保存计算,只是个规则的识别)。

例如Mapper组件自带4个插入函数API:

那么我们在使用该实体,且在执行该函数API时候就能够具备自动填充值能力了。

按照如上几个步骤完成我们的扩展能力,我们使用者能够带来足够的好处:

★高级使用人员可以自定义一个字段级的注解,同时可以在注解上指定一些参数;然后将值生成的实现封装起来,保持依赖最低原则;

★应用者仅需要配置注解就可以完成自定义值的生成,使用简单的同时,仅仅只是依赖于一个注解,不依赖于实现,低耦合;

★将多种实现与注解关联统一的配置,有利于模块内核级别的管理和维护性;

案例:

 

030304、扩展机制之MapperProvider


MapperProvider 扩展机制是本Mapper组件的最重要的一个扩展核心。整个上述提供的所有的Mapper的API都是基于该核心提供的基础套件,也就是MapperProvider内核设计是不提供一个针对使用者的Mapper的API。

MapperProvider的设计原理,请参考设计文档,本章节主要介绍使用和扩展。

①定义一个Mapper接口的函数API

我们可以定义一个或者多个Mapper接口,并实现@com.cenboomh.hit.framework.mybatis.mapper.annotation.RegisterMapper标记接口,该接口主要用于标记使用者定义了个扩展Mapper接口。

注:同一个Mapper接口中的函数,其指定的Provider类,必须是同一个,如果是不同的,请分开定一个。

在使用者定义的Mapper接口中,自定义需要的通用函数API(使用者的API函数名称不允许重复,不支持重载,因为Mybatis的MappedStatement设计原理决定的)。

例如:

@RegisterMapper

public interface SelectMapper {

 

    /**

     * 根据实体中的属性值进行查询,查询条件使用等号

     *

     * @param record 查询条件对象

     * @return 查询结果集合

     * @see BaseSelectProvider#select(MappedStatement)

     */

    @SelectProvider(type = BaseSelectProvider.class, method = DYNAMIC_SQL)

    List select(T record);

}

在自定义的函数上使用Mybatis自带的4种Provider注解中的一种:@SelectProvider @InsertProvider @DeleteProvider @UpdateProvider。

注:Mybatis自带的Provider功能,本章节不详细介绍,请自行查阅Mybatis的相关功能,本核心功能就是利用了Mybatis的Provider功能,其Provider功能是利用运行期,根据入参进行反射的。而Mapper组件采用的预编译方式执行。因此如果使用任然需要使用原生态的Mybatis的Provider能力,那么请不要在该接口类上定义@RegisterMapper。

②构造MapperProvider

针对上述定义的使用者函数API,我们需要定一个与之对应的MapperProvider,同时需要继承com.cenboomh.hit.framework.mybatis.mapper.mapperhelper.BaseMapperProvider,以便获取更多帮助。

例如:

/**

 * BaseSelectProvider实现类,基础方法实现类

 *

 * @author songzou

 * @date 2018/7/17

 */

public class BaseSelectProvider extends BaseMapperProvider {

    /**

     * 根据Mapper类,以及mapperHelper构造一个模板对象

     *

     * @param mapperClass  Mapper类

     * @param mapperHelper MapperHelper

     */

    public BaseSelectProvider(Class mapperClass, MapperHelper mapperHelper) {

        super(mapperClass, mapperHelper);

    }

 

    /**

     * 构造SQL片段

     *

     * @param ms MappedStatement

     * @return SQL语句片段

     * @see com.cenboomh.hit.framework.mybatis.mapper.common.base.select.SelectMapper#select(Object)

     */

    public String select(MappedStatement ms) {

        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.whereAllIfColumns(entityClass, isNotEmpty()));

        sql.append(SqlHelper.orderByDefault(entityClass));

        return sql.toString();

    }

}

针对上述的定义有如下几个约束:

★Mapper中定义的函数名,要与Provider中的保持一致;

★Mapper中定义的函数的注解上Provider类,为当前的扩展的Provider类,且其method应该指定为"dynamicSQL" 或者 com.cenboomh.hit.framework.mybatis.mapper.mapperhelper.BaseMapperProvider.DYNAMIC_SQL;

★扩展的Provider必须继承BaseMapperProvider,且拥有对应的2个入参的构造函数。BaseMapperProvider类中以及提供了一个函数为 public String dynamicSQL(Object record)  空实现,主要用于启动器能够被Mybatis原生态的Provider所识别;

★扩展的Provider中的函数签名的入参必须为MappedStatement对象(Mybatis的一个对象),以及出参必须为String,Void, SqlNode(Mybatis的一个对象),一般常用为String;

★扩展的Provider中的函数实现,也即利用MappedStatement对象,生成一个预编译的SQL片段;

类似于的预编译SQL片段锦集欣赏:

SELECT id,countryname,countrycode  FROM country 

<where>

    <if test="id != null"> AND id = #{id}if>

    <if test="countryname != null"> AND countryname = #{countryname}if>

    <if test="countrycode != null"> AND countrycode = #{countrycode}if>

where>

SELECT id,countryname,countrycode  FROM country 

<where

    AND id = #{id}

where>

UPDATE country 

<set>

    id = id,

    <if test="countryname != null">countryname = #{countryname},if>

    <if test="countrycode != null">countrycode = #{countrycode},if>

set>

<where

    AND id = #{id}

where>

只要扩展者能够通过MappedStatement对象构造出来如上的SQL片段,严格意义来说是Mybatis的SQL片段(也即sqlmap中sql执行节点,只是我们一般都是写死的)即可扩展出任何你想要的功能。

③使用扩展的函数API

Mapper组件的内置的扩展Mapper,采用了一个统一的Mapper接口,全部接管继承了,因此使用者仅需要继承Mapper接口即可(基础章节中已经介绍)。

那么应用者自己定义的该怎么办呢?因为Java语言的接口是允许多重继承的,因此可以自行继承扩展的Mapper即可,多少个都行,唯一的要求就是不能函数名重复。

如果应用者扩展的API,有足够的通用性,就可以上升到Mapper组件内置中来(也就是使Mapper接口自带继承,而不需使用者额外继承),以便丰富整个Mapper生态圈。

已经足够足够简单,具体使用参考基础API相关章节。

030305、扩展机制之HackInterceptor


HackInterceptor 主要是类似于Mybatis提供的Plugin技术的Interceptor,只是Mybatis提供的这个插件技术主要用于Mybatis组件中四大重要处理器 Executor、StatementHandler、ParameterHandler、ResultSetHandler。

这里就不介绍Mybatis提供的。HackInterceptor主要是在 Mapper 和 Session 之间提供的一套插件技术,并且是一个调用链条,也就是支持多层级HackInterceptor调用,技术原理和Mybatis基本相同,但是其介入的时机比 Executor更早。

能够带来什么呢?

我们可以在Mapper接口(对使用者的API)上增加方法级别的注解,由Mapper组件根据注解匹配原则,进行多层拦截。拦截后,可以根据自己需要进行任意的处理逻辑。

类似于在Mapper接口上,增加AOP机制,但是这个机制不依赖于Spring环境,能够和Session密切相关,也可以根据需要按注解筛选拦截。

先根据目前Mapper内置的两个HackInterceptor ,来介绍下使用方法,以便更多大牛丰富它。

HackInterceptor之Batch

 批量更新的使用已经在上述章节中介绍了。不论是Mapper组件内置的批量更新,还是使用者针对老代码需要的批量更新,都是利用了在Mapper层进行拦截。

原理:

首先拦截所有拥有@BatchProvider注解的函数API,根据具体的函数API,解析出需要批量更新的参数对象的位置以及类型;

然后根据Mybatis内置的ExecutorType.Batch(当然我们以及内置绕过Mybatis3.0+的批量bug)进行循环单个调用,分批执行;

最后见执行的结果进行汇总返回。

而这一切都是建立在一个共同的使用规则上即可完成。这个规则就是扩展者要来制定的,一旦按照这个规则来制定的函数API即可具备该功能。

HackInterceptor之GeneratorKey

值生成的使用已经在上述章节中介绍了。同样,不论Mapper内置还是老代码都可以使用,因其利用的就是Mapper层进行拦截。

该拦截器的拦截链条比批量更新更早。

原理:

首先拦截所有拥有@GeneratorKeyProvider注解的函数API,根据具体的函数API,解析出需要值生成的参数对象的位置以及类型规则;

然后根据规则类型中定义的值生成规则的扩展种类,查找对应的值生成实现;

再然后根据规则实现,进行调用批量填充值的实现进行值填充;

最后交由下一个拦截器进行处理(没有就直接转交给Mybatis组件进行处理);

小节:利用HackInterceptor的多层级链条调用,配合Mybatis组件,结合其拦截时机,可以完成很多执行SQL的前置能力,而这些能力所需要配置,都可以通过注解方式来解决,已达到灵活的扩展能力。

下面介绍下扩展HackInterceptor的几个步骤。

①定义方法级注解

我们可以定义任意的一个注解,用于方法级可以,这里简单就不介绍了。

②定义拦截器

先介绍下com.cenboomh.hit.framework.mybatis.mapper.hack.HackInterceptor的API。

public interface HackInterceptor {

 

    /**

     * 拦截处理

     *

     * @param invocation 调用对象

     * @return 拦截的结果对象

     * @throws Throwable 执行异常

     */

    Object intercept(HackInvocation invocation) throws Throwable;

}

扩展者定义一个实现类,并实现intercept方法。最简单的使用如:return invocation.proceed();  也即什么也不做,直接交由下一个拦截器处理。API的详细介绍参见:实现拦截器

我们需要让该拦截器进行识别。

类似于Mybatis的Interceptor,我们使用

@HackIntercepts(value = @HackSignature(GeneratorKeyProvider.class), order = -100)

public class GeneratorKeyInterceptor implements HackInterceptor {

如上方式进行识别。一个拦截器可以识别多个方法级注解的签名(但是我们一般很少这么干)。

同时还可以指定该拦截器的执行优先级,order的值越小越先执行。形成类似于HttpFilter样的链条式处理结构。

按照这种方式配置表明,如果一个Mapper的函数API上声明了@GeneratorKeyProvider,那么它将会被该拦截器所拦截。

如果我们需要拦截Mapper上的所有函数API,我们可以采用@HackIntercepts(value = {}, order = -10000)的方式表示,但是你真的需要这么做吗?

③使用注解以及配置拦截器

在任意一个需要拦截器的函数API上,添加上定义的方法级注解。

将扩展的实现类的实例增加到MapperConfiguration对象里面,如:void addHackInterceptor(HackInterceptor interceptor) 。Mapper组件以及扩展了Mybatis的Configuration,使用Mapper组件后,任意位置取到的Configuration其实例都是MapperConfiguration。

如果使用非Spring环境,那么可以在构造SqlSessionFactory以后,获取并配置MapperConfiguration。

如果使用的是Spring环境,那么可以使用 进行注入。

警告:同一个实现类,只能注入一次实例,不论实例的对象是否为同一个。注入多次后,只有第一次生效,其它无效,也不会报错。

④实现拦截器

注解已定义,实现已注入。我们还需要让该实现类能够识别到指定注解的函数上进行链式拦截。通过上述三步就已经准备就绪。

本小节介绍下HackInterceptor拦截器的com.cenboomh.hit.framework.mybatis.mapper.hack.HackInvocation实例,能用来做什么吧。

public interface HackInvocation {

 

    /**

     * 代理对象目标,一般拦截对尽量不使用该方法进行对象操作

     *

     * @return 代理对象目标

     */

    Object getTarget();

 

    /**

     * 执行拦截的方法对象

     *

     * @return 拦截的方法对象

     */

    Method getMethod();

 

    /**

     * 拦截方法的实际入参对象,拦截对象可以修改该对象的内容

     *

     * @return 实际入参对象

     */

    Object[] getArgs();

 

    /**

     * Mapper SqlSession 代理对象

     *

     * @return SqlSession 代理对象

     * @see MapperSqlSessionProxy#getConfiguration()

     * @see MapperSqlSessionProxy#openActualSession()

     * @see MapperSqlSessionProxy#releaseActualSession(SqlSession)

     */

    MapperSqlSessionProxy getSessionProxy();

 

    /**

     * 执行下一个调用链条,当前拦截器处理完毕后,可以交由下一个处理器处理

     *

     * @return 处理结果

     * @throws Throwable 执行发生错误

     */

    Object proceed() throws Throwable;

}

★可以根据拦截器提供的方法,入参等数据,进行自定义修改数据,例如:值生成与填充;

★可以根据拦截器提供的方法,入参以及MapperSqlSessionProxy对象获取Session,进行SQL直接操作,例如:批量更新;

★还可以记录日志?方法执行的性能? 等等,只有你想不到,没有办不到的,最起码它是一个AOP。

警告:为了保障拦截器扩展的执行性能,本功能并没有类似与Mybatis的Plugin技术的Interceptor使用多层JDK代理,只进行了一次JDK代理。然后将所有的拦截器按照链式结构进行组装,然后依次调用,算法复杂度O(1),因此不允许对HackInvocation对象的getTarget()返回的实例进行反射调用,这样会破坏调用链条(除非使用者已经明确知道自己再干什么)。同时也不能对同一个HackInvocation对象进行多次proceed调用

 

你可能感兴趣的:(tk-mybatis使用)