关于数据库:如何规避MyBatis使用过程中带来的全表更新风险

8次阅读

共计 9749 个字符,预计需要花费 25 分钟才能阅读完成。

作者:京东批发 贾玉西

一、前言

程序员 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 蕴含了 <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>    
    <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 进行下个执行链的解决,对业务流程无影响,代码为证:

正文完
 0