如何规避MyBatis使用过程中带来的全表更新风险
做者:京东零售 贾玉西
一、媒介
法式员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.施行查询语句
ListUser 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包罗了mappers/mappers标签节点,那里就是MyBatis映射器。也能够理解为mappers/mappers标签下设置装备摆设的各类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
groupIdorg.slf4j/groupId
artifactIdslf4j-api/artifactId
version${slf4j.version}/version
scopeprovided/scope
/dependency
dependency
groupIdp6spy/groupId
artifactIdp6spy/artifactId
version${p6spy.version}/version
/dependency
dependency
groupIdorg.mybatis/groupId
artifactIdmybatis/artifactId
version${mybatis.version}/version
scopeprovided/scope
/dependency
dependency
groupIdorg.mybatis/groupId
artifactIdmybatis-spring/artifactId
version${mybatis-spring.version}/version
scopeprovided/scope
exclusions
exclusion
groupIdorg.mybatis/groupId
artifactIdmybatis/artifactId
/exclusion
/exclusions
/dependency
dependency
groupIdcom.github.jsqlparser/groupId
artifactIdjsqlparser/artifactId
version${jsqlparser.version}/version
/dependency
dependency
groupIdorg.springframework/groupId
artifactIdspring-core/artifactId
version${spring.core.version}/version
scopeprovided/scope
/dependency
4.2 项目中引进避免全表更新依靠SDK
dependency
groupIdcom.jd.o2o/groupId
artifactIdo2o-mybatis-interceptor/artifactId
version1.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以英文逗号分隔设置装备摆设
MapString,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停止下个施行链的处置,对营业流程无影响,代码为证:
我来回答