mybatis 逆向工程使用姿式不对,把表清空了,内心慌的一比,因而写了个插件。

使用mybatis逆向工程的时候,delete方法的使用姿式不对,致使表被清空了,在生产上一刷新后发现表里没数据了,一股凉意从脚板心直冲天灵盖。html

因而开发了一个拦截器,并写下这篇文章记录并分享。java

这锅只能本身背了

你用过 mybatis 逆向工程(mybatis-generator-maven-plugin)生成相关文件吗?git

就像这样式儿的:github

能够看到逆向工程帮咱们生成了实体类、Mapper 接口和 Mapper.xml。web

用起来真的很方便,我用了好几年了,可是前段时间翻车了。面试

具体是怎么回事呢,我给你们摆一下。spring

先说一下需求吧。就是在作一次借据数据迁移的过程当中,要先经过 A 服务的接口拿到全部的借据和对应的还款计划数据,而后再对这些借据进行核查,若是不知足某些添加,就须要从表中删除借据和对应的还款计划。sql

借据和对应的还款计划存放在两张表中,用借据号来关联。数据库

而上线以后,我在一片欢声笑语中把还款计划表清空了,而这个必现的问题,在测试阶段同窗尚未测试出来。apache

事情发生后我赶忙找到了 DBA 协助修复数据:

是怎么回事呢,为了模拟这个场景,我在本地建立了两张表,订单表(orderInfo)和订单扩展表(orderInfoExt),他们之间用订单号进行关联:

仅仅是作演示,因此两张表是很是简单的,

咱们假设如今表里面的这条订单号为 2020060666666 的数据通过判断是错误数据,我当时写的代码体如今单元测试里面是这样的:

看出问题了吗?

第 42 行用的 example 对象仍是 OrderInfo 的 example。而真正的 OrderInfoExt 对象的 exampleExt 对象没有进行任何赋值的操做。

为何会出现这样的乌龙呢?

都怪 idea 太智能了!(强行找个借口)

我只须要打一个 ex 而后回个车.... example 就出如今代码里面了。

而这种没有参数的 example 传进去,在 mapper.xml 里面是这样处理的:

执行一下,看看效果:

看到 delete from order_info_ext 语句。你说你慌不慌?

固然在线上的服务器确定是看不到执行的 SQL 的,可是当报警短信一条一条接着来的时候,当连上数据库一看表,发现数据没了的时候。

你说你慌不慌?

反正我一刷新后发现表里没数据了,一股凉意从脚板心直冲天灵盖。这种时候都仍是要小小的心慌一下,先大喊一声“卧槽!数据怎么没了?”

而后赶忙报备,准备找 DBA 捞数据吧。

还好,本次误删不影响正常业务。

数据恢复过程就不说了,聊一下这事发生后个人一点思考吧。

哦,对了,还得说一下测试同窗为何没有发现这个问题。这个问题确实是一个必现的问题,测试案例上也写了这个测试点。

可是测试同窗查看数据的时候用的是 select 语句,查询条件给的是确实须要被删除的数据 。

而后分别在两个表里面执行后发现:数据确实是没了。

是的,是数据确实是没了。整个表都干净了。

看着测试妹子惶恐不安的样子,我还能怎么说呢?

这锅,不甩了,我本身背下来吧。

从新审视逆向工程

咱们先看看逆向工程帮咱们生成的接口:

我相信用过 mybatis 逆向工程的朋友们,一看到这几个接口就知道了:哟,这都是老朋友了。

当我再去从新审视这些接口的时候我会发现其实还有会有一些问题的。

好比 delete 这样的高危语句咱们仍是须要尽可能的手写 xml。

好比 updateByExample 一样存在因为误操做没有 where 条件,致使全表更新的状况。

好比 select 语句是查出了整个对象,可是有时间咱们可能只须要对象里面的某个值而已。

好比 select 语句针对大表、关键表操做的时候,不能从代码的角度限定 SQL 必须带上索引字段查询。

上面的这些问题咱们怎么处理呢?

个人建议是不要使用 mybatis 的逆向工程,全都手写。

开个玩笑。咱们确定不能因噎废食,况且逆向工程确实是帮咱们作了不少工做,极大的方便咱们这样的 CRUD Boy 进行 CRUD。

因此,我想 mybatis 的逆向工程确定是有什么配置来控制生成哪些接口的,别问为何,问就是直觉。

由于要是让我去开发这样的一个插件,我确定也会提供对应的开关配置。

我如今的想法是不让它给我生成 delete 相关的接口,这个接口用起来我内心惧怕。

因此怎么配置呢?

咱们去它的 DTD 文件里面找一下嘛:

这个文件不长,一共也才 213 行,你能发现这一块东西:

你用脚指头想也能知道,这就是咱们要找的开关配置。从 DTD 文件的描述中来看,这个几个参数是配置在 table 标签里面的。

咱们去试一下:

果真是这样的。而后咱们进行相关配置以下:

再生成一下:

果真,delete 相关的接口没了。

而后咱们程序中真的须要 delete 操做的时候,再本身去手写 xml 文件。

那你本身写的 xml 文件也忘记写 where 条件了这么办?

这个月工资别领了。本身好好反思反思。

固然,就算你真的忘记写了,下面这个拦截器还能给你兜个底,帮你一把。

mybatis 拦截器使用

其实这个方案是我想到的第一个方案。致使上面问题的缘由很简单嘛,就是执行了delete 语句却没有 where 条件。

那么咱们能够拦截到这个 SQL 语句,而后对其进行两个判断:

是不是 delete 语句。 若是是,是否包含 where 条件。

那么问题来了,咱们怎么去拦截到这个 SQL 呢?

答案就是咱们能够开发一个 mybatis 插件呀,就像分页插件那样。

插件,听起来很高端的样子,其实他就是个拦截器。实现起来很是简单。

先去官网上看一下:

中文:https://mybatis.org/mybatis-3/zh/configuration.html#plugins

英文:https://mybatis.org/mybatis-3/configuration.html

在官网上,对于插件这一模块的描述是这样的:

经过 MyBatis 提供的强大机制,使用插件是很是简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名便可。

正如官网说的这样,插件开发、使用起来是很是简单的。只须要三步:

1.实现 Interceptor 接口。

2.指定想要拦截的方法签名。

3.配置这个插件。

mybatis 插件开发

基于上面这三步,你们先看一下咱们这插件怎么写,以及这个插件的效果。

先说明一下本文涉及到的源码 mybatis 版本是 3.4.0。

本文用拦截器的目的是判断 delete 语句中是否有 where 条件。因此,开发出来的插件长这样:

再来一个复制粘贴直接运行版本:

@Slf4j @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), }) public class CheckSQLInterceptor implements Interceptor {
private static String SQL_WHERE = "where";

@Override
public Object intercept(Invocation invocation) throws Throwable {
    //获取方法的第0个参数,也就是MappedStatement。@Signature注解中的args中的顺序
    MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
    //获取sql命令操做类型
    SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
    final Object[] queryArgs = invocation.getArgs();
    final Object parameter = queryArgs[1];
    BoundSql boundSql = mappedStatement.getBoundSql(parameter);
    String sql = boundSql.getSql();
    if (SqlCommandType.DELETE.equals(sqlCommandType)) {
        //格式化sql
        sql = sql.replace("\n", "");
        if (!sql.toLowerCase().contains(SQL_WHERE)) {
            sql = sql.replace(" ", "");
            log.info("删除语句中没有where条件,sql为:{}", sql);
            throw new Exception("删除语句中没有where条件");
        }
    }
    return invocation.proceed();
}

@Override
public Object plugin(Object o) {
    return Plugin.wrap(o, this);
}

@Override
public void setProperties(Properties properties) {
}
复制代码
复制代码private static String SQL_WHERE = "where"; @Override public Object intercept(Invocation invocation) throws Throwable { //获取方法的第0个参数,也就是MappedStatement。@Signature注解中的args中的顺序 MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; //获取sql命令操做类型 SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); final Object[] queryArgs = invocation.getArgs(); final Object parameter = queryArgs[1]; BoundSql boundSql = mappedStatement.getBoundSql(parameter); String sql = boundSql.getSql(); if (SqlCommandType.DELETE.equals(sqlCommandType)) { //格式化sql sql = sql.replace("\n", ""); if (!sql.toLowerCase().contains(SQL_WHERE)) { sql = sql.replace(" ", ""); log.info("删除语句中没有where条件,sql为:{}", sql); throw new Exception("删除语句中没有where条件"); } } return invocation.proceed(); } @Override public Object plugin(Object o) { return Plugin.wrap(o, this); } @Override public void setProperties(Properties properties) { } 复制代码} 复制代码

再把插件注册上(注册插件还有其余的方法,后面会讲到,这里只是展现Bean注入的方式):

咱们先看看配上插件后的执行效果:

能够看到日志中输出了:

删除语句中没有where条件,sql为:delete from order_info_ext

并抛出了异常。

这样,咱们的扩展表的数据就保住了。在测试阶段,测试同窗就必定能扯出来问题,瞟一眼日志就明白了。

就算测试同窗忘记测试了,在生产上也不会执行成功,抛出异常后还会有报警短信通知到相应的开发负责人,及时登上服务器去处理。

功能实现了,确实是很是的简单。

咱们再说回代码,你说说看:当你拿到上面这段代码后,最迷惑的地方是哪里?

其中的逻辑是很简单的了。 没有什么特别的地方,我想大多数人拿到这段代码迷惑的地方在于这个地方吧:

这个 @Intercepts 里面的 @Signature 里面为何要这样配置?

咱们先看看 @Intercepts 注解:

里面是个数组,能够配置多个 Signature。因此,其实这样配置也是能够的:

关键的地方在于 @Signature 怎么配置:

这个问题,咱们放到下一节去讨论。

mybatis插件的原理

上面一小节咱们知道了对于开发插件而言,难点在于 @Signature 怎么配置。

其实这也不能叫难点,只能说你不知道能配置什么,比较茫然而已。这一小节就来回答这个问题。

要知道怎么配置就必需要了解mybatis 这四大对象:Executor、ParameterHandler 、ResultSetHandler 、StatementHandler 。

官网上说:

MyBatis 容许你在映射语句执行过程当中的某一点进行拦截调用。默认状况下,MyBatis 容许使用插件来拦截的方法调用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

ParameterHandler (getParameterObject, setParameters)

ResultSetHandler (handleResultSets, handleOutputParameters)

StatementHandler (prepare, parameterize, batch, update, query)

那官网上说的这四大对象分别是拿来干啥用的呢?

Executor:Mybatis 的执行器,用于进行增删改查的操做。

ParameterHandler :参数处理器,用于处理 SQL 语句中的参数对象。

ResultSetHandler:结果处理器,用于处理 SQL 语句的返回结果。

StatementHandler :数据库的处理对象,用于执行SQL语句

知道拦截的四大对象了,咱们就能够先揭秘一下上面的这个注解配置的是啥了:

type 字段存放的是 class 对象,其取值范围就是上面说的四大对象。

method 字段存放的是 class 对象的具体方法。

args 存放的是具体方法的参数。

看到这几个参数你想到了什么?有没有条件反射式的想到反射?若是没有的话你再咂摸咂摸,看看能不能品出一点反射的味道。

本文用拦截器的目的是判断 delete 语句中是否有 where 条件,所以通过上面的分析,Executor 对象就能知足咱们的需求。

因此在本文示例中 @Signature 的 type 字段就是 Executor.class。

那 method 字段咱们放哪一个方法呢?放 delete 吗?

这就得看看 Executor 对象的方法有哪些:

能够看到其中并无 delete 方法,和 SQL 执行相关的,看起来只有 query和 update。

可是,咱们能够大胆猜想一下呀:delete 也是一种 update。

接着去求证一下就行:

能够看到 delete 方法确实是调用了 update 方法。

因此在本文案例中 @Signature 的 method 字段放的是 update 方法。

已经知道具体的方法了,那 args 放的就是方法的入参,因此这段配置就是这样来的:

真的,我以为这属于手摸手教学系列了。通过这个简单的案例,我但愿你们能作到一通百通。

接下来带你们看看咱们经常使用的分页插件 pageHelper 是怎么作的吧。

其实你用脚指头也能想到,分页插件确定是拦截的查询方法,咱们只是须要去验证一下就能够。

引入 pageHelper 后能够看到 Interceptor 的多了两个实现:

咱们看一下 PageInterceptor 方法吧:

对吧,拦截了两个 query 方法,一个参数是 4 个,一个参数是 6 个:

同时,在 intercept 的实现里面有一部分是这样写的:

4 个参数和 6 个参数是作了单独处理的,至于为何要这样处理,至于为何要拦截两个 query 方法,提及来又是一个很长的故事了。

详细的能够看看这个连接: https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md

好了,仍是那句话:若是要写出好的 mybatis 插件,必须知道 @Signature 怎么去配置。配置后能拦截哪些东西,你内心应该是有点数的。

mybatis插件的原理

前面咱们知道拦截器怎么写了,接下来简单的分析一波原理。

前几天我看到一个观点是说看开源框架的源码建议从 mybatis 看起。我是很同意这个观点的,确实是优雅,而容易看懂。能品出不少设计模式的使用。

一句话总结 mybatis插件的原理就是:动态代理加上责任链。

先看一下 Plugin 类的动态代理:

标号为 ① 的地方一看就知道,InvocationHandler,JDK 动态代理,没啥说的。

标号为 ② 的地方是 wrap 方法,生成 Plugin 代理对象。

标号为 ③ 的地方是 invoker 方法,圈起来的目的是想说是在这里判断当前方法是不是须要被拦截的方法。若是是则用代理对象走拦截器逻辑,若是不是则用目标对象,走正常逻辑。

给你们看一下这个地方的 debug 效果:

一个平平无奇的 if 判断,是拦截器的关键。为何这个地方多说了几句呢?

由于其实这就是细节的地方。当面试的时候面试官问你:mybatis 是怎么判断是否须要拦截这个方法的时候你能答上来。说明你是真的看过源码。

责任链是怎么体现的呢?

就是这个地方: org.apache.ibatis.plugin.InterceptorChain

你看又学到一招,mybatis 里面的设计模式还有责任链。

咱们看一下 pluginAll 方法的调用方:

这个地方就体现出以前官网说的了:

插件是做用于这四大对象的:Executor、ParameterHandler 、ResultSetHandler 、StatementHandler 。

上面框起来的这四个框,就是插件调用的地方。

那么插件在何时被加载,或者说什么是被注册上的呢?

仍是回到拦截链这个类上去:

pluginAll 方法咱们已经知道有哪些地方调用了。这个方法里面其实还有两个考点。

第一就是 interceptor 这个 List 集合的定义,用了 final 修饰。因此要注意 final 修饰基本类型和引用类型的区别,被 final 修饰的引用类型变量内部的内容是能够发生变化的。

第二就是 getInterceptors 返回的是一个不可修改的 List 。因此,要对集合 interceptors 进行修改,只能经过 addInterceptor 方法进行元素添加,保证了这个集合是可控的。

因此,咱们只须要知道哪里调用了 addInterceptor 方法,哪里就是插件被注册的地方。

一个是 SqlSessionFactoryBean ,一个是 XMLConfigBuilder。

使用 XML 配置是这样的:

熟悉 mybatis 的朋友们确定知道,无非就是对于标签的解析而已。

解析到 plugins 标签,则进入 pluginElement 方法中,在这个方法里面调用 addInterceptor:

本文没有使用 XML 的形式配置,因此咱们主要看一下 SqlSessionFactoryBean。

怎么看呢?

不要盲目的走入源码,加个断点看调用链,跟着调用链去走就很清晰了。

在这个地方加一个断点:

而后 debug 起来,你就能够看到整个调用链了:

而后咱们根据上面的调用链,咱们就能够找到源头了:

在 MybatisAutoConfiguration 的构造方法里面初始化了 interceptors。

而 interceptorsProvider.getIfAvailable() 方法也解释了为何咱们只须要在程序里面这样注入咱们的拦截器就能够被找到了:

对 getIfAvailable 方法不熟悉的朋友能够去补一下这块的知识,我这里只是给你们看一下这个方法上的注释:

固然,你这样去注入的话有可能会不生效,你就会大骂一声:写的什么垃圾玩意,配置上了也不对呀。

别着急呀,我还没说完呢。你看看是否是有自定义的 SqlSessionFactory 在项目里。

看一下注入 SqlSessionFactory 的源码上面的那个注解了吗?

@ConditionalOnMissingBean ,看名字也知道了,当你的项目里面没有自定义的 SqlSessionFactory 的时候,才会由源码给你注入,这个时候才会正在的注册上插件:

若是你有自定义的 SqlSessionFactory,那么请手动调用 factory.setPlugins 方法。

因此,总结一下插件的三种配置方法:

1.xml方式配置。

2.若是没有自定义 SqlSessionFactory 直接 @Bean 注入拦截器便可。

3.若是有自定义 SqlSessionFactory 须要在自定义的地方手动调用 factory.setPlugins 方法。

其实我尝试过第四种方法,在application.properties 里面配置:

这种配置方式才是符合 SpringBoot 思想的配置。才是真正的丝滑,润物无声的丝滑。

惋惜,我配置上后,点击到对应的源码地方一看:

它调用的是 getInterceptors 方法,我就知道确定是有问题了:

果真,运行起来会报这样的错误: Failed to bind properties under 'mybatis.configuration.interceptors' to java.util.List<org.apache.ibatis.plugin.Interceptor>

找了一圈缘由,最后发现了这个 issue:

github.com/mybatis/spring-boot-starter/issues/180

这个“奇异博士”头像的用户提出了和我同样的问题:

而后下面的回答是这样的:

别问,问就是不支持。请使用 @Bean 的方式。

最后说一句(求关注)

点个“赞”吧,周更很累的,不要白嫖我,须要一点正反馈。

才疏学浅,不免会有纰漏,若是你发现了错误的地方,还请你指出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。

欢迎关注个人微信公众号:why技术。在这里我会分享一些java技术相关的知识,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评、影评。感谢你的关注,愿你我共同进步。

相关文章
相关标签/搜索