通用Mapper插件和MyBatis拦截器的使用
通用Mapper和MyBatis拦截器的使用MyBatis拦截器拦截器的作用MyBatis 拦截器的简单实现实例结果验证orderBy默认排序通用Mapper的使用MyBatis拦截器拦截器的作用在实际开发过程中我们对于数据库中的表经常会有大量重复工作,例如在业务表中都会存在以下字段:// 创建时间private LocalDateTime createTime;// 创建人private Str
通用Mapper和MyBatis拦截器的使用
MyBatis拦截器
拦截器的使用
在实际开发过程中我们对于数据库中的表经常会有大量重复工作,例如在业务表中都会存在以下字段:
// 主键
private String id;
// 创建时间
private LocalDateTime createTime;
// 创建人
private String createBy;
// 更新时间
private LocalDateTime updateTime;
// 更新人
private String updateBy;
// 删除标识
private String delFlag;
查询:在对业务数据进行查询时,所有表都需要增加 delFlag=0 的筛选条件。
大多数时候还需要将数据按createTime倒序排序。
新增:在对业务数据进行新增时,需要对delFlag、createTime、createBy、updateTime和updateBy进行赋值。
如果主键使用自定义生成方式,还需要调用生成方法进行赋值
修改:在对业务数据进行修改时,需要对updateTime和updateBy进行赋值。
删除:在对业务数据进行删除时,实际上进行的是update操作,是将目标数据的delFlag置为1。
面对以上大量的重复工作,我们可以使用MyBatis拦截器进行自动化实现。
MyBatis 拦截器的简单实现
自定义拦截器实现接口Interceptor:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Component
public class MpcMybatisInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 StatementHandler ,默认是 RoutingStatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取 StatementHandler 包装类
MetaObject metaObjectHandler = SystemMetaObject.forObject(statementHandler);
// 获取查询接口映射的相关信息
MappedStatement mappedStatement = (MappedStatement) metaObjectHandler.getValue("delegate.mappedStatement");
// 获取请求时的参数
Object parameterObject = statementHandler.getParameterHandler().getParameterObject();
// 获取sql
String sql = showSql(mappedStatement.getConfiguration(), mappedStatement.getBoundSql(parameterObject));
// TODO 这里可对SQL语句进行转换处理
// 此处代码较长,已省略
String newSql=originalSql;
metaObject.setValue("delegate.boundSql.sql", newSql);
return invocation.proceed();
}
}
/**
* 进行?的替换
*/
public static String showSql(Configuration configuration, BoundSql boundSql) {
// 获取参数
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// sql语句中多个空格都用一个空格代替
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (CollectionUtils.isNotEmpty(parameterMappings) && parameterObject != null) {
// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 如果根据parameterObject.getClass()可以找到对应的类型,则替换
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
// MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 该分支是动态sql
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(obj)));
} else {
// 未知参数,替换?防止错位
sql = sql.replaceFirst("\\?", "unknown");
}
}
}
}
return sql;
}
通过以上代码,将需要执行的SQL进行加工处理,可以完成增删改查的SQL转换。
实例结果验证
转化前SQL:
SELECT id,examine_date,examine_user_name,examine_user_id,blast_examine_volume,create_time,create_by,update_time,update_by,del_flag,remark,examine_plan_number,blast_area_number,excavation_area FROM t_examine_blast LIMIT ?
转化后SQL:
SELECT id, examine_date, examine_user_name, examine_user_id, blast_examine_volume, create_time, create_by, update_time, update_by, del_flag, remark, examine_plan_number, blast_area_number, excavation_area FROM t_examine_blast WHERE t_examine_blast.del_flag = '0' LIMIT ?
支持多表关联的SQL注入:
转化前SQL:
select * from (select * from bbb where id = '123') a , (select * from ddd) as b where a.id = '123'
转化后SQL:
SELECT * FROM (SELECT * FROM bbb WHERE (id = '123') AND bbb.del_flag = '0') a, (SELECT * FROM ddd WHERE ddd.del_flag = '0') AS b WHERE a.id = '123'
orderBy createTime默认排序
orderBy createTime的默认排序由于postgresql与mysql语法不一致,目前暂未完成注入。
注:因为 count(0)与orderBy同时使用时postgresql会报语法错误,而mysql可以正常执行
拦截器在项目中的使用
1.为兼容已完成代码,对现有功能模块不产生影响,需要将使用拦截器的类进行声明。
在com.mpc.common.mybatis.MybatisInterceptor类中将使用拦截器的模块或类添加到列表中。
在列表中的类执行的sql语句均会进行SQL注入。
private List<String> excludeStatement =
Arrays.asList("com.mpc.examine.mapper",
"com.mpc.quality.mapper"
);
2.确保使用拦截器的业务表(实体)中,必须包含以下字段,否则会引起SQL错误
注:如果进行关联查询,则要求关联表中必须也含有del_flag 字段
// 创建时间
private LocalDateTime createTime;
// 创建人
private String createBy;
// 更新时间
private LocalDateTime updateTime;
// 更新人
private String updateBy;
// 删除标识
private String delFlag;
3.模块的pom.xml文件引入拦截器的Maven依赖
<dependency>
<groupId>com.mpc</groupId>
<artifactId>mpc-common-mybatis</artifactId>
</dependency>
目前项目内拦截器实现了以下功能
select语句默认增加del_flag='0’的过滤,支持多表关联
update语句默认为updateTime和updateBy字段赋值
insert语句默认为id、 createTime、createBy、updateTime、updateBy和delFlag字段赋值
ID生成策略默认使用UUID生成规则
注:拦截器不再生成ID策略,ID生成使用通用Mapper的主键策略实现
delete语句未做修改
注:删除标识的更新使用通用Mapper的deleteFlag()方式实现,见通用Mapper案例
通用Mapper
简介
简单的说,通用Mapper可以是说是对Mybatis-generator代码生成器的一种升级。
先说说Mybatis-generator的缺点:
- 当数据库字段变化很频繁,就需要反复重新生成代码,并且由于 MBG 覆盖生成代码和追加方式生成 XML,导致每次重新生成都需要大量的比对修改
- 仅仅基础的增删改查等方法,就已经产生了大量的 XML 内容,还没有添加一个自己手写的方法,代码可能就已经几百行了,内容多,看着比较碍事。
- 批量操作,批量插入,批量更新,需要自写。
因为很多人都在使用 MBG,MBG 中定义了很多常用的单表方法,为了解决前面提到的问题,也为了兼容 MBG 的方法避免项目重构太多,在 MBG 的基础上结合了部分 JPA 注解产生了 通用 Mapper 。
通用 Mapper 可以很简单的让你获取基础的单表方法,也很方便扩展通用方法。使用通用 Mapper 可以极大的提高你的工作效率。
- 通用mapper提供一系列的增删改查的通用方法,再进行单表的增删改查时,只需要调用对应通用方法就可以忽略表字段的更改对mapper方法的影响,而且只需要一个通用Mapper,不需要每张表都对应一个Mapper。
- 通用Mapper是对单表的CRUD操作进行了较为详细的实现,使得开发人员可以随意的按照自己的需求选择通用的方法,同时允许开发人员便捷地对通用Mapper进行扩展。
- 通用Mapper同样有Example的设计,与MGB不同的是,MDB会对每一个表生成对应的Example类,而通用Mapper提供了一个统一的Example类,这个类和 MBG 生成的相比,需要自己设置属性名,这个类还额外提供了更多的方法。
使用教程
通用Mapper在项目中的使用
1.模块的pom.xml文件引入自定义Mapper的Maven依赖
<dependency>
<groupId>com.mpc</groupId>
<artifactId>mpc-common-mybatis</artifactId>
</dependency>
2.业务Mapper对通用Mapper进行继承,例如:
package com.mpc.examine.mapper;
import com.mpc.common.mybatis.BaseMapper;
import com.mpc.examine.domain.TDispatchRotaryDrill;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TDispatchRotaryDrillMapper extends BaseMapper<TDispatchRotaryDrill> {
}
@Autowired
private TDispatchRotaryDrillMapper tDispatchRotaryDrillMapper;
3.现在就可以使用全部的通用Mapper和自定义方法了
通用Mapper中PostgreSQL的适配
主键策略
如果使用数据库自增主键时,需要在实体中设置主键的insertable 属性,在生成insert动态SQL时,忽略ID字段,因为postgresql在insert时不允许主键为null,而使用mysql就没有这个问题
设置useGeneratedKeys参数值为true,在执行添加记录之后可以获取到数据库自动生成的主键ID。
@Id
@KeySql(useGeneratedKeys = true)
@Column(insertable = false)
private Long Id;
使用UUID作为主键时,可以在实体类中指定主键生成类,UUIdGenId类中来定义主键生成规则,在数据插入前生成主键都可以使用这个方式,不限于UUID
@Id
@KeySql(genId = UUIdGenId.class)
private String id;
public class UUIdGenId implements GenId<String> {
@Override
public String genId(String s, String s1) {
return UUID.randomUUID().toString().replace("-", "");
}
}
也可以使用数据库生成的UUID来进行回写,方式如下:
@Id
@KeySql(sql = "select gen_random_uuid()", order = ORDER.BEFORE)
private String id;
order = ORDER.BEFORE表示在插入之前执行sql来获取主键ID,这里还需要使用到postgresql的gen_random_uuid()函数,在postgresql13以上版本中已经提供了生成UUID数据的内置函数。如果使用13之前的版本,需要手动扩展:UUID函数扩展方法
使用Weekend代替Example
主要区别在于进行:赋予相关sql语句的条件时:
Example使用的是字符串和对应数据的方法;
Weekend使用的是JDK8特性的stream操作,使用双冒号把方法当做参数传到stream内部。
使用 Example 时,需要自己输入属性名,例如
“countryname”,假设输入错误,或者数据库有变化,这里很可能就会出错,因此基于 Java 8
的方法引用是一种更安全的用法,如果你使用 Java 8,你可以试试 Weekend。
Weekend weekend = new Weekend(TestDangerousSourceBase.class);
WeekendCriteria<TestDangerousSourceBase, Object> criteria = weekend.weekendCriteria();
criteria.andEqualTo(TestDangerousSourceBase::getDangerousSourceName, tDangerousSourceBase.getDangerousSourceName());
TestDangerousSourceBase tDangerousSourceBase = tDangerousSourceBaseMapper.selectOneByExample(weekend);
自定义Mapper在项目中的应用
在我们项目中,经常需要展示执行相同结构的查询语句,操作起来重复率较高的,可以使用通用Mapper的自定义查询解决。
案例1:关联查询更新人和登陆人姓名
在业务展示页面内经常需要展示数据的创建人和更新人,但是我们在业务表数据库中存放的是系统用户表的主键ID,那么在需要显示姓名时,每次都需要将业务表与系统用户表进行关联查询。
1.首先在业务实体类中增加字段
注:@Transient表示非数据库字段,一般情况下,实体中的字段和数据库表中的字段是一一对应的,但是也有很多情况我们会在实体中增加一些额外的属性,这种情况下,就需要使用 @Transient 注解来告诉通用 Mapper 这不是表中的字段。
@Transient
@ApiModelProperty(value = "更新人姓名", hidden = true)
private String updateByName;
@Transient
@ApiModelProperty(value = "创建人姓名", hidden = true)
private String createByName;
2.增加通用Mapper方法
/**
* 通用Mapper
*
* @param <T>
* @author liaoyuxing
*/
public interface BaseMapper<T> extends CustomMapper<T>, Mapper<T>, InsertListMapper<T> {
}
/**
* 自定义通用Mapper
*
* @param <T>
* @author liaoyuxing
*/
@RegisterMapper
public interface CustomMapper<T> {
// Example查询扩展,查出创建人和更新人姓名
@SelectProvider(type = CustomMapperProvider.class, method = "dynamicSQL")
List<T> selectUserByExample(Object example);
}
3.然后继承MapperTemplate ,实现selectUserByExample方法
实现思路与selectByExample方法基本一致,只是在Columns中增加了两行代码
/**
* 自定义通用mapper方法的provider
*
* @author liaoyuxing
*/
public class CustomMapperProvider extends MapperTemplate {
public CustomMapperProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
/**
* 在Example查询基础上,增加创建人姓名和更新人姓名的查询
*
* @param ms
* @return
*/
public String selectUserByExample(MappedStatement ms) {
Class<?> entityClass = getEntityClass(ms);
//将返回值修改为实体类型
setResultType(ms, entityClass);
StringBuilder sql = new StringBuilder("SELECT ");
if (isCheckExampleEntityClass()) {
sql.append(SqlHelper.exampleCheck(entityClass));
}
sql.append("<if test=\"distinct\">distinct</if>");
//支持查询指定列
sql.append(SqlHelper.exampleSelectColumns(entityClass));
// ---add---
sql.append(",(select user_name from sys_user AS createUser where createUser.user_id::text = " + tableName(entityClass) + ".create_by) as createByName ");
sql.append(",(select user_name from sys_user AS updateUser where updateUser.user_id::text = " + tableName(entityClass) + ".update_by) as updateByName ");
// ---add end---
sql.append(SqlHelper.fromTable(entityClass, tableName(entityClass)));
sql.append(SqlHelper.exampleWhereClause());
sql.append(SqlHelper.exampleOrderBy(entityClass));
sql.append(SqlHelper.exampleForUpdate());
return sql.toString();
}
4.在业务层使用查询
@Mapper
public interface TDispatchRotaryDrillMapper extends BaseMapper<TDispatchRotaryDrill> {
}
/**
* Service业务层处理
*
* @author liaoyuxing
* @date 2021-03-15
*/
@Service
public class TDispatchRotaryDrillServiceImpl implements TDispatchRotaryDrillService {
@Autowired
private TDispatchRotaryDrillMapper tDispatchRotaryDrillMapper;
@Override
public R getList(PageVo<TDispatchRotaryDrill> vo) {
Example example = new Example(TDispatchRotaryDrill.class);
PageHelper.startPage(vo.getPageNum(), vo.getPageSize(), vo.getOrderBy());
List<TDispatchRotaryDrill> tDispatchRotaryDrillList = tDispatchRotaryDrillMapper.selectUserByExample(example);
return R.ok(new PageInfo<>(tDispatchRotaryDrillList));
}
}
5.结果验证
拼装完成的SQL:
SELECT id, blast_area_number, hole_total, plan_arrive_time, dispatch_user, dispatch_time, rotary_drill_number, del_flag, create_by, create_time, update_by, update_time, remark, (SELECT user_name FROM sys_user AS createUser WHERE createUser.user_id::text = t_dispatch_rotary_drill.create_by) AS createByName, (SELECT user_name FROM sys_user AS updateUser WHERE updateUser.user_id::text = t_dispatch_rotary_drill.update_by) AS updateByName FROM t_dispatch_rotary_drill WHERE (((blast_area_number LIKE ?))) AND t_dispatch_rotary_drill.del_flag = '0' LIMIT ?
接口返回结果:
{
"code": 200,
"msg": null,
"data": {
"total": 54,
"list": [
{
"id": "46de4ca9be8b4367a5433ee380b56ce3",
"blastAreaNumber": "BP_20101010_B758",
"holeTotal": "33",
"planArriveTime": "2021-03-03 17:12:31",
"dispatchUser": "王五",
"dispatchTime": "2021-03-04 11:43:26",
"rotaryDrillNumber": "2020063142501202",
"delFlag": "0",
"createBy": "1",
"createTime": "2021-03-22 13:46:00",
"updateBy": "1",
"updateTime": "2021-03-22 13:46:00",
"remark": "张三",
"updateByName": "admin",
"createByName": "admin",
"params": {}
},
以下省略...
案例2:自动更新删除标识
在软件开发过程中,一般我们对业务数据不使用物理删除,而是使用逻辑删除。因为物理删除不但会影响后续查询效率,还会有业务数据丢失的风险,也不利于后期维护排查BUG。
那么每次更新标识时都需要进行setDelFlag(“1”),这里也可以使用通用Mapeer实现。
这里仅展示CustomMapperProvider类代码,其余代码与案例1基本相同。
注意:通过主键进行更新标识,一定要在实体类中对主键增加@Id注解,否则通用Mapper会将所有字段作为联合主键进行更新
/**
* 通过主键将del_flag置为‘1’
*
* @param ms
* @return
*/
public String deleteFlag(MappedStatement ms) {
Class<?> entityClass = getEntityClass(ms);
StringBuilder sql = new StringBuilder();
sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
sql.append(new StringBuffer().append("<set>").append(delFlagColumn).append("= '1' </set>"));
sql.append(SqlHelper.wherePKColumns(entityClass, true));
return sql.toString();
}
Mapper的其他解决方案
MyBatis Plus
实际上,MyBatis Plus(MP)拥有比通用Mapper更加强大的功能,还内置了逻辑删除、分页等实用功能。
此外,MP还支持通用枚举、Sql注入、代码生成器等功能。
但是,在已经有开发完成的业务模块的基础上,MyBatis Plus相对通用Mapper来讲重构成本较高
通用Mapper可以兼容Mybatis-generator 的方法避免项目重构太多,基本可以达到无痕接入的目的
总结一下,通用Mapper是对Mybatis-generator的升级改造,解决了使用Mybatis-generator可能需要大量重构的问题,并且在这个基础上加入了一些新的功能。Mybatis-Plus可以看作是在另一个方向上对Mybatis的升级改造,不仅能够根据数据库表快速生成pojo实体类,还封装了大量CRUD方法,使用Wrapper解决了复杂条件构造等问题,更是根据开发中常见的问题给出了一系列解决方案。
在拥有Maven和Spring boot的开发框架下,MBG、通用Mapper和MP都可以快速地完成安装,相比于MBG和通用Mapper仅需要执行插件就可以完成基本的开发工作,MP可能需要更多的开发工作量。
更多推荐
所有评论(0)