写点什么

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

  • 2023-03-14
    北京
  • 本文字数:7519 字

    阅读完需:约 25 分钟

如何规避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.执行查询语句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 项目使用方式: 配置类中加入拦截器配置


@Configurationpublic 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 进行下个执行链的处理,对业务流程无影响,代码为证:



发布于: 2023-03-14阅读数: 13
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
如何规避MyBatis使用过程中带来的全表更新风险_数据库_京东科技开发者_InfoQ写作社区