在实际开发过程中我们对于数据库中的表经常会有大量重复工作,例如在业务表中都会存在以下字段:
// 主键
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拦截器进行自动化实现。
自定义拦截器实现接口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的默认排序由于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可以是说是对Mybatis-generator代码生成器的一种升级。
先说说Mybatis-generator的缺点:
因为很多人都在使用 MBG,MBG 中定义了很多常用的单表方法,为了解决前面提到的问题,也为了兼容 MBG 的方法避免项目重构太多,在 MBG 的基础上结合了部分 JPA 注解产生了 通用 Mapper 。
通用 Mapper 可以很简单的让你获取基础的单表方法,也很方便扩展通用方法。使用通用 Mapper 可以极大的提高你的工作效率。
通用 Mapper教程
通用 Mapper 实现原理
github地址
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和自定义方法了
如果使用数据库自增主键时,需要在实体中设置主键的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函数扩展方法
主要区别在于进行:赋予相关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的自定义查询解决。
在业务展示页面内经常需要展示数据的创建人和更新人,但是我们在业务表数据库中存放的是系统用户表的主键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
* @author liaoyuxing
*/
public interface BaseMapper<T> extends CustomMapper<T>, Mapper<T>, InsertListMapper<T> {
}
/**
* 自定义通用Mapper
*
* @param
* @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("distinct ");
//支持查询指定列
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": {}
},
以下省略...
在软件开发过程中,一般我们对业务数据不使用物理删除,而是使用逻辑删除。因为物理删除不但会影响后续查询效率,还会有业务数据丢失的风险,也不利于后期维护排查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("" ).append(delFlagColumn).append("= '1' "));
sql.append(SqlHelper.wherePKColumns(entityClass, true));
return sql.toString();
}
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可能需要更多的开发工作量。