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的缺点:

  1. 当数据库字段变化很频繁,就需要反复重新生成代码,并且由于 MBG 覆盖生成代码和追加方式生成 XML,导致每次重新生成都需要大量的比对修改
  2. 仅仅基础的增删改查等方法,就已经产生了大量的 XML 内容,还没有添加一个自己手写的方法,代码可能就已经几百行了,内容多,看着比较碍事。
  3. 批量操作,批量插入,批量更新,需要自写。

因为很多人都在使用 MBG,MBG 中定义了很多常用的单表方法,为了解决前面提到的问题,也为了兼容 MBG 的方法避免项目重构太多,在 MBG 的基础上结合了部分 JPA 注解产生了 通用 Mapper 。
通用 Mapper 可以很简单的让你获取基础的单表方法,也很方便扩展通用方法。使用通用 Mapper 可以极大的提高你的工作效率。

  • 通用mapper提供一系列的增删改查的通用方法,再进行单表的增删改查时,只需要调用对应通用方法就可以忽略表字段的更改对mapper方法的影响,而且只需要一个通用Mapper,不需要每张表都对应一个Mapper。
  • 通用Mapper是对单表的CRUD操作进行了较为详细的实现,使得开发人员可以随意的按照自己的需求选择通用的方法,同时允许开发人员便捷地对通用Mapper进行扩展。
  • 通用Mapper同样有Example的设计,与MGB不同的是,MDB会对每一个表生成对应的Example类,而通用Mapper提供了一个统一的Example类,这个类和 MBG 生成的相比,需要自己设置属性名,这个类还额外提供了更多的方法。

使用教程

通用 Mapper教程

通用 Mapper 实现原理

github地址

通用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可能需要更多的开发工作量。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐