作者:京东零售 贾玉西

一、前言

程序员A: MyBatis用过吧?

程序员B: 用过

程序员A: 好巧,我也用过,那你遇到过什么风险没?比如全表数据被更新或者删除了。

程序员B: 咔,还没遇到过,这种情况需要跑路吗?

程序员A: 哈哈,不至于。但使用过程中,由于业务数据校验不当,确实可能会造成全表更新或者删除。

程序员B: 喔,吓死我了,我们都是好人,不会做删库跑路类似蠢事,能展开讲讲这个风险怎样造成的吗?

程序员A: 好的,你能看出下面这段代码会有风险吗?

程序员B: 平时大家都这样写的,也没看出啥风险呀!

程序员A: 假如DAO层没做非空校验,relationId字段传入为空,这段代码组装出来的是什么语句?

程序员B: update cms_relation_area_code set yn = 1 where yn = 0 我擦,全表被逻辑删除了!哥哥,我们的web应用数量多,代码行数几十万行,你怎么处理的呀,不会人力梳理代码吧?得累死…

程序员A: 昂,可以的,基于MyBatis的扩展点可以实现一款插件做到降低全表更新的风险,降低人工成本。

程序员B: 哥哥,要不讲讲MyBatis和实现的插件?

程序员A: 那必须嘞,技术是需要分享和互补的。

不知大家在使用MyBatis有没有过程序员A哥哥遇到的事件?好巧,本人也经历过跟程序员A小哥哥一样的境遇,初始思路也是人工梳理代码,后来经由架构师点拨能不能开发一款SDK统一处理,要不然就扛着身体去梳理这几十万行代码了。要不一起聊聊这块,共同成长~

一起先看下MyBatis原理吧?当然这部分比较枯燥,本篇文章也不会大废篇幅去介绍这块,简单给大家聊下基本流程,对MyBatis原理不感兴趣的同学可以直接跳到第三章往后看

那… 第二章我就简单开始淡笔介绍MyBatis了,在座各位好友没啥意见吧,想更深入了解学习,可以读下源码,或者阅读下京东架构-小傅哥手撸MyBatis专栏博客(地址:bugstack.cn)

二、MyBatis 原理

先来看下MyBatis执行的概括执行流程,就不逐步贴源码了,东西实在多…

//1.加载配置文件
InputStream inputStream =Resources.getResourceAsStream(“mybatis-config.xml”);
//2.创建 SqlSessionFactory 对象(实际创建的是 DefaultSqlSessionFactory 对象)
SqlSessionFactory builder =newSqlSessionFactoryBuilder().build(inputStream);
//3.创建 SqlSession 对象(实际创建的是 DefaultSqlSession 对象)
SqlSession sqlSession = builder.openSession();
//4.创建代理对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//5.执行查询语句
List<User> users = mapper.selectUserList();
//释放资源
sqlSession.close();
inputStream.close();

mybatis整个执行流程,可以抽象为上面5步核心流程,咱们这里只讲解XML开发的方式,注解的方式基本核心思想一致:

第一步:读取mybatis-config.xml配置文件。转化为流,这一步没有需要细说的。

第二步:创建SqlSessionFactory 对象。 实际创建的是DefaultSqlSessionFactory对象,这里SqlSessionFactory和DefaultSqlSessionFactory的关系为:SqlSessionFactory是一个接口,DefaultSqlSessionFactory是该接口的一个实现,也是利用了Java的多态特性。SqlSessionFactory是MyBatis中的一个重要的对象,汉译过来可以叫做:SQL会话工厂,见名知意,它是用来创建SQL会话的一个工厂类,它可以通过SqlSessionFactoryBuilder来获得,SqlSessionFactory是用来创建SqlSession对象的,SqlSession就是SQL会话工厂所创建的SQL会话。并且SqlSessionFactory是线程安全的,它一旦被创建,应该在应用执行期间都存在,在应用运行期间(也就是Application作用域)不要重复创建多次,建议使用单例模式。

第三步:创建 SqlSession 对象。 实际创建的是 DefaultSqlSession 对象,这里同上步,SqlSession为接口,DefaultSqlSession为SqlSession接口的一个实现类,SqlSession的主要作用是用来操作数据库的,它是MyBatis 核心 API,主要用来执行命令,获取映射,管理事务等。SqlSession虽然提供select/insert/update/delete方法,在旧版本中使用使用SqlSession接口的这些方法,但是新版的Mybatis中就会建议使用Mapper接口的方法,也就是下面要讲到的第四步操作。SqlSession对象,该对象中包含了执行SQL语句的所有方法,类似于JDBC里面的Connection。在JDBC中,Connection不直接执行SQL方法,而是生成Statement或者PrepareStatement对象,利用Statement或者PrepareStatement来执行增删改查方法;在MyBatis中,SqlSession可以直接执行增删改查方法,可以通过提供的 selectOne、 insert等方法,也可以获取映射器Mapper来执行增删改查操作,通过映射器Mapper来执行增删改查如第四步代码所示。这里需要注意的是SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域。

第四步:创建代理对象。 SqlSession一个重要的方法getMapper,顾名思义,这个方法是用来获取Mapper映射器的。什么是MyBatis映射器?MyBatis框架包括两种类型的XML文件,一类是配置文件,即mybatis-config.xml,另外一类是操作DAO层的映射文件,例如UserInfoMapper.xml等等。在MyBatis的配置文件mybatis-config.xml包含了标签节点,这里就是MyBatis映射器。也可以理解为标签下配置的各种DAO操作的mapper.xml的映射文件与DaoMapper接口的一种映射关系。映射器只是一个接口,而不是一个实现类。可能初学者可能会产生一个很大的疑问:接口不是不能运行吗?的确,接口不能直接运行,但是MyBatis内部运用了动态代理技术,生成接口的实现类,从而完成接口的相关功能。所以在第四步这里 MyBatis 会为这个接口生成一个代理对象。

第五步:执行SQL操作以及释放连接操作。

Emmm… 再补张图吧,刚刚的介绍感觉还没开始就结束了,通过下面这张图我们再深入了解下MyBatis整体设计(此图借鉴京东架构-小傅哥手撸MyBatis专栏)

第一步:读取Mybatis配置文件。

第二步:创建SqlSessionFactory对象。 上面已经对SqlSessionFactory做了说明,但SqlSessionFactoryBuilder具体还没描述,SqlSessionFactoryBuilder是构造器,见名知意,它的主要作用便是构造SqlSessionFactory实例,基本流程为根据传入的数据流创建XMLConfigBuilder,生成Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例。XMLConfigBuilder主要作用是解析mybatis-config.xml中的标签信息,如图中列举出的两个标签信息,解析环境信息及mapper.xml信息,解析mapper.xml时,Mybatis默认XML驱动类为XMLLanguageDriver,它的主要作用是解析select、update、insert、delete节点为完整的SQL语句,也是对应SQL的解析过程,XMLLanguageDriver在解析mapper.xml时,会将解析结果存储至SqlSource的实现类中,SqlSource是一个接口,只定义了一个 getBoundSql() 方法,它控制着动态 SQL 语句解析的整个流程,它会根据从 Mapper.xml 映射文件解析到的 SQL 语句以及执行 SQL 时传入的实参,返回一条可执行的 SQL。它有三个重要的实现类,对应图中写到的RawSqlSource、DynamicSqlSource及StaticSqlSource,其中RawSqlSource处理的是非动态 SQL 语句,DynamicSqlSource处理的是动态 SQL 语句,StaticSqlSource是BoundSql中要存储SQL语句的一个载体,上面RawSqlSource、DynamicSqlSource的SQL语句,最终都会存储到StaticSqlSource实现类中。StaticSqlSource的 getBoundSql() 方法是真正创建 BoundSql 对象的地方, BoundSql 包含了解析之后的 SQL 语句、字段、每个“#{}”占位符的属性信息、实参信息等。这里也重点介绍下Configuration对象,Configuration 的创建会装载一些基本属性,如事务,数据源,缓存,代理,类型处理器等,从这里可以看出 Configuration 也是一个大的容器,来为后面的SQL语句解析和初始化提供保障,也是Mybatis中贯穿全局的存在,后续我们要提到的Mybatis降低全表更新插件,也是基于这个对象来完成。其中解析mapper.xml这步最终作用便是将解析的每一条CRUD语句封装成对应的MappedStatement存放至Configuration中。

第三步:创建SqlSession对象。 创建过程中会创建另外两个东西,事务及执行器,SqlSession可以说只是一个前台客服,真正发挥作用的是Executor,它是 MyBatis 调度的核心,负责 SQL 语句的生成以及查询缓存的维护,对SqlSession方法的访问最终都会落到Executor的相应方法上去。Executor分成两大类:一类是CachingExecutor,另一类是普通的Executor。CachingExecutor是在开启二级缓存中用到的,二级缓存是慎开启的,这里只介绍普通的Executor,普通的Executor分为三大类,SimpleExecutor、ReuseExecutor和BatchExecutor,他们是根据全局配置来创建的。SimpleExecutor是一种常规执行器,也是默认的执行器,每次执行都会创建一个Statement,用完后关闭;ReuseExecutor是可重用执行器,将Statement存入map中,操作map中的Statement而不会重复创建Statement;BatchExecutor是批处理型执行器,专门用于执行批量sql操作。总之,Executor最终是通过JDBC的java.sql.Statement来执行数据库操作。

第四步:获取Mapper代理对象。 上面也已经提到了这块用到的是jdk动态代理技术,这里MapperRegistry和MapperProxyFactory在解析mapper.xml已经被创建保存在了Configuration中,这步主要就是从MapperProxyFactory获取MapperProxy代理。其中MapperMethod主要的功能是执行SQL的相关操作,它根据提供的Mapper的接口路径,待执行的方法以及配置Configuration作为入参来执行对应的MappedStatement操作。

第五步:执行SQL操作。 这步就是执行执行对应的MappedStatement操作,Executor最终是通过JDBC的java.sql.Statement来执行数据库操作。但其实真正负责操作的是StatementHanlder对象,StatementHanlder封装了JDBC Statement 操作,负责对 JDBC Statement 的操作,它通过控制不同的子类,去执行完整的一条SQL执行与解析的流程。

三、MyBatis拦截器

Mybatis一共提供了四大扩展点,也称作四大拦截器插件,它是生成层层代理对象的一种责任链模式。这里代理的实现方式是将切入的目标处理器与拦截器进行包装,生成一个代理类,在执行invoke方法前先执行自定义拦截器插件的逻辑从而实现的一种拦截方式。每个处理器在Mybatis的整个执行链路中扮演的角色也不同,大家如果有想法可以基于这几个扩展点实现一款自己的拦截器插件。例如我们常用的一个分页插件pageHelper就是利用Executor拦截器实现的,有兴趣的可以自行阅读下pageHelper源码。MyBatis一共提供了四个扩展点:

Executor (update, query, ……)

Executor根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用。创建JDBC的Statement连接对象,传递给StatementHandler对象。这里Executor又称作 SQL执行器

· StatementHandler (prepare, parameterize, ……)

StatementHandler对于JDBC的PreparedStatement类型的对象,创建的过程中,这时的SQL语句字符串是包含若干个 “?” 占位符。这里StatementHandler又称作SQL 语法构建器

· ParameterHandler (getParameterObject, ……)

ParameterHandler用于SQL对参数的处理,这步会通过TypeHandler将占位符替换为参数值,接着继续进入PreparedStatementHandler对象的query方法进行查询。这里ParameterHandler又称作参数处理器

· ResultSetHandler (handleResultSets, ……)

ResultSetHandler进行最后数据集(ResultSet)的封装返回处理。这里ResultSetHandler又称作结果集处理器

四、MyBatis防止全表更新插件

上面说到程序员A小哥哥遇到过历史业务参数因校验问题造成了全表更新的风险,梳理代码成本又过高,不符合当下互联网将本增效的理念。那么有没有一种成本又低,效率又高,又能通用的产品来解决此类问题呢?

当然有了!!! 不然这篇帖子搁这凑绩效呢? 哈哈… 不好笑不好笑,见谅。

第三章节中,提到MyBatis为使用者提供了四个扩展点,那么我们就可以借助扩展点来实现一个Mybatis防止全表更新的插件,具体怎么实现呢?这里博主是使用StatementHandler拦截器抽象出来一个SDK供需求方接入,拦截器具体用法参考度娘,这里SDK实现流程为:获取预处理SQL及参数值 --> 替换占位符组装完整SQL --> SQL语句规则解析 --> 校验是否为全表更新SQL。 当然还做了一些横向扩展,这里放张图吧,更清晰些。

那么这个插件能拦截哪些类型的SQL语句呢?

·无where条件:update/delete table ·逻辑删除字段:update/delete table where yn = 0  //yn为逻辑删除字段·拼接条件语句:update/delete table where 1 = 1·AND条件语句:update/delete table where 1 = 1 and 1 <> 2·OR 条件语句:update/delete table where 1 = 1 or 1 <> 2

然后聊下怎么接入吧:

4.1 检查项目依赖

scope为provided的请在项目中加入该jar包依赖,此插件默认引入p6spy、jsqlparser依赖,如遇版本冲突请排包

<dependency>    <groupId>org.slf4j</groupId>    <artifactId>slf4j-api</artifactId>    <version>${slf4j.version}</version>    <scope>provided</scope>
</dependency>
<dependency>    <groupId>p6spy</groupId>    <artifactId>p6spy</artifactId>    <version>${p6spy.version}</version>
</dependency>
<dependency>    <groupId>org.mybatis</groupId>    <artifactId>mybatis</artifactId>    <version>${mybatis.version}</version>    <scope>provided</scope>
</dependency>
<dependency>    <groupId>org.mybatis</groupId>    <artifactId>mybatis-spring</artifactId>    <version>${mybatis-spring.version}</version>    <scope>provided</scope>    <exclusions>        <exclusion>            <groupId>org.mybatis</groupId>            <artifactId>mybatis</artifactId>        </exclusion>    </exclusions>
</dependency>
<dependency>    <groupId>com.github.jsqlparser</groupId>    <artifactId>jsqlparser</artifactId>    <version>${jsqlparser.version}</version>
</dependency>
<dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-core</artifactId>    <version>${spring.core.version}</version>    <scope>provided</scope>
</dependency>

4.2 项目中引入防止全表更新依赖SDK

<dependency>    <groupId>com.jd.o2o</groupId>    <artifactId>o2o-mybatis-interceptor</artifactId>    <version>1.0.0-SNAPSHOT</version>
</dependency>

4.3 项目中添加配置

springboot项目使用方式: 配置类中加入拦截器配置

@Configuration
public class MybatisConfig {    @Bean    ConfigurationCustomizer configurationCustomizer() {        return new ConfigurationCustomizer() {            @Override            public void customize(org.apache.ibatis.session.Configuration configuration) {                FullTableDataOperateInterceptor fullTableDataOperateInterceptor = new FullTableDataOperateInterceptor();                //表默认逻辑删除字段,按需配置,update cms set name = "zhangsan" where yn = 0,yn为逻辑删除资源,此语句被认为是全表更新语句                fullTableDataOperateInterceptor.setLogicField("yn");                //白名单表,按需配置,配置的白名单表不拦截该表全表更新操作                fullTableDataOperateInterceptor.setWhiteTables(Arrays.asList("tableName1","tableName2"));                                //个别表的逻辑删除字段映射,如果配置此项,此表逻辑删除字段优先走该表配置,key为表名,value为该表的逻辑删除字段名,每对key-value以英文逗号分隔配置                Map<String,String> tableToLogicFieldMap = new HashMap<>();                tableToLogicFieldMap.put("tableName3","ynn");                tableToLogicFieldMap.put("tableName4","ynn");                fullTableDataOperateInterceptor.setTableToLogicFieldMap(tableToLogicFieldMap);                //配置拦截器                configuration.addInterceptor(fullTableDataOperateInterceptor);            }        };    }
}

传统SSM项目使用方式: 在mybatis.xml中追加plugin配置

<configuration>      <plugins>        <plugin interceptor="com.jd.o2o.cms.mybatis.interceptor.FullTableDataOperateInterceptor">            //表默认逻辑删除字段,按需配置,update cms set name = "zhangsan" where yn = 0,yn为逻辑删除字段,此语句被认为是全表更新语句            <property name="logicField" value="yn"/>            //白名单表,按需配置,配置的白名单表不拦截该表全表更新操作            <property name="whiteTables" value="tableName1,tableName2"/>            //个别表的逻辑删除字段映射,如果配置此项,此表逻辑删除字段优先走该表配置,key为表名,value为该表的逻辑删除字段名,每对key-value以英文逗号分隔配置            <property name="tableToLogicFieldMap" value="key1:value1,key2:value2"/>        </plugin>    </plugins>
</configuration>

4.4 添加日志输出

该插件有四处输出error日志,具体可看源码

<Logger name="com.jd.o2o.cms.mybatis.interceptor" level="error" additivity="false">    <AppenderRef ref="RollingFileError"/>
</Logger>

4.5 性能及接入说明

大家最关心的可能是,接入这个SDK后,对我们数据库操作的性能有多大影响,这里针对性能做下说明:

•select:无性能影响

•insert:不足千分之一毫秒

•update:约为0.02毫秒

•delete:约为0.02毫秒

然后就是对接入的风险的考虑,如果为该插件解析过程中的异常,该插件直接catch交由MyBatis进行下个执行链的处理,对业务流程无影响,代码为证:

如何规避MyBatis使用过程中带来的全表更新风险相关推荐

  1. HoloLens 2 打包发布过程中 常见问题汇总(长期更新)

    HoloLens 2 打包发布过程中 常见问题汇总(长期更新)                   目 录 1. Unity 中发布Hololens的时候界面变灰,提示无发布权限. 2. Unity发 ...

  2. oracle避免回表,请教大家: 经常在sql调优过程中提到避免“回表”,具体是指什么意思???3Q...

    你的位置: 问答吧 -> Oracle -> 问题详情 请教大家: 经常在sql调优过程中提到避免"回表",具体是指什么意思???3Q 如题,不甚感激!!!! [ 本帖 ...

  3. update过程中遇到kb2829760补丁无法更新而导致vs安装失败的解决方法: 1、安装KB2829760; 2、安装KB2829760中文语言包; 3、安装VS2013 with upda

    update过程中遇到kb2829760补丁无法更新而导致vs安装失败的解决方法: 1.安装KB2829760: 2.安装KB2829760中文语言包: 3.安装VS2013 with update. ...

  4. mysql数据库表删了重建error_数据库内核月报 - 2015 / 09-MySQL · 捉虫动态 · 建表过程中crash造成重建表失败-阿里云开发者社区...

    问题描述 主库的create table语句传到备库,备库SQL线程执行过程中报错: Error 'Can't create table 'XXX.XX' (errno: -1)' on query. ...

  5. oracle表连接——处理连接过程中另外一张表没有相关数据不显示问题

    一个数据表基本上很难满足我们的查询要求,同时,将所有的数据都保存在一个表格中显然也不是一种好的数据库设计,为了避免数据的冗余,删除.更新异常,我们通常需要建立一张外键表,通过表连接,来获取我们自己想要 ...

  6. oracle不显示表和过程,oracle表连接——处理连接过程中另外一张表没有相关数据不显示问题...

    一个数据表基本上很难满足我们的查询要求,同时,将所有的数据都保存在一个表格中显然也不是一种好的数据库设计,为了避免数据的冗余,删除.更新异常,我们通常需要建立一张外键表,通过表连接,来获取我们自己想要 ...

  7. Delphi中ADO查询多表更新单表解决方法

    学Delphi也好久了,但一直没怎么总结,这不好久没弄ADO了,趁这个星期一直看公司的代码,自己也来实践一下.以前一直用ADO操作单表,一直没怎么弄过ADO操作多表的程序,不自己实践一下还真不知道操作 ...

  8. Mybatis逆向工程过程中出现targetRuntime in context mybatisGenerator is invalid

    最开始设置的Mybatis,但是逆向工程准备就绪后出现问题 报错为targetRuntime in context mybatisGenerator is invalid 后来修改为Mybatis3能 ...

  9. SQL Server 2008 安装过程中遇到“性能计数器注册表”..

    Windows 2008 系统 SQL Server 2008 性能计数器注册表 作者:        来源:        时间:2010-6-13              完美集成.增强 Kin ...

最新文章

  1. huffman树和huffman编码
  2. 天马行空W:在C++中调用DLL中的函数
  3. wxWidgets:wxHeaderCtrl类用法
  4. 机器学习:决策树过拟合与剪枝,决策树代码实现(三)
  5. UVa 1625 Color Length
  6. 【转载】Python操作Excel的读取以及写入
  7. Python面试题总结(8)--操作类
  8. matlab中now函数_now()方法以及JavaScript中的示例
  9. 均分纸牌(洛谷-P1031)
  10. Pair智能标注神器,全面升级,更加智能
  11. EventBus,轻松实现跨组件跨线程通信
  12. php导入csv 进度条,php 导入导出怎么做成有进度条??
  13. java基础篇(二) ----- java面向对象的三大特性之继承
  14. 手机触摸屏有电阻屏和电容屏,有什么区别?
  15. 阅读了Steve Yegge的文章。其中有一篇叫“Practicing Programming”(练习编程),写成于2005年
  16. 物理层-宽带接入技术
  17. Quartz 定时任务管理
  18. 《XSS跨站脚本攻击剖析与防御》—第6章6.1节参 考 文 献
  19. 用计算机处理数据ppt,全国“创新杯”说课大赛计算机应用基础类优秀作品 :处理数据说课课件.ppt...
  20. 基于DxOMark的影像质量客观测试研究

热门文章

  1. 微软官方自己精简的XP系统--WIN XP基础版WinFLPCs
  2. java调试器jdb_Java调试教程-Java调试器(JDB)概述
  3. 用PC上的Chrome 模拟手机浏览器
  4. 机器学习简介 | GAIR大讲堂
  5. 【学习笔记】【Liblinear】
  6. Calico-介绍、原理与使用
  7. 医疗机器人在AI领域的广泛应用
  8. 说说ESXi虚拟交换机和端口组的“混杂模式”
  9. Sangfor_NGAF学习笔记4
  10. uniapp 在线预览文档