聊聊 Mybatis 动态 SQL

这篇文章,我们聊聊 Mybatis 动态 SQL ,以及我对于编程技巧的几点思考 ,希望对大家有所启发。

1 什么是 Mybatis 动态SQL

如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。

Mybatis 借助功能强大 OGNL 表达式,可以根据参数条件,动态生成执行 SQL 。

使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。

这条语句提供了可选的查询博客文章列表 ,如果不传入 "title",那么所有处于 "ACTIVE" 状态的 博客都会返回。

如果传入了 "title" 参数,那么就会对 "title" 一列进行模糊查找并返回对应的 BLOG 结果。

如果希望通过 "title" 和 "author" 两个参数都可以搜索,只需要加入另一个条件即可,见下图:

我们也可以使用 where 标签,该标签只会在子元素返回任何内容的情况下才插入 "WHERE" 子句。而且,若子句的开头为 "AND" 或 "OR",where 标签也会将它们去除。

Mybatis 还支持 choose (when, otherwise)、trim (where, set)、foreach 等其他的动态标签,这里就不一一赘述了。

2 人生第一次线上OOM事故

我曾服务一家电商公司的用户中心,用户中心提供用户注册,查询,修改等基础功能 。

那个时候 Dubbo 等 RPC 框架并没有开源,所有的服务都以 HTTP 接口形式提供,数据传输格式是 XML 。

因为写接口非常费劲,所以为了接口复用,我写了一个通用接口 getUserByConditions ,该接口支持通过 「用户名」、「昵称」、「手机号」、「用户编号」这三个查询用户基本信息。

使用的是 ibatis (mybatis 的前身), SQLMap 见下图 。

当构建动态 SQL 查询时,条件通常会追加到 WHERE 子句后,而以 WHERE 1 = 1 开头,可以轻松地使用 AND 追加其他条件。

用户中心在上线后,竟然每隔三四个小时就发生了内存溢出问题 ,经过通过和 DBA 沟通,发现高频次出现全表查用户表,执行 SQL 变成 :

查看日志后,发现前端传递的参数出现了空字符串 ,笔者在代码中并没有做参数校验,所以才出现全表查询 ,当时用户表的数据是 1000多万 ,页面调用几次,用户中心服务就 OOM 了。

最终解决问题的方式很简单,后端在接收参数时,做了参数校验。

事故虽然解决了,但对我的影响一直延续至今。我仍然记得当时站在运维同学旁边,不断的看他调整 JVM 参数 ,重启服务的画面,自己那个时候真的是羞愧难当,心中发誓:以后绝对不可以发生类似的事故

对于动态 SQL ,我的编程思维也经历了如下三个阶段 :

  • 前后端参数校验

  • 复用和专用要做平衡

  • 防御性编程意识

3 前后端参数校验

为了提升开发效率,我们人为的将系统分为前端、后端,分别由两拨不同的人员开发 ,经常出现系统问题时,两拨人都非常不服气,相互指责。

要想系统健壮,前后端应该同时做接口参数校验 (后端必须做参数校验),当大家都遵循这个规约时,出现系统问题的风险大大减少。

1、前端校验

前端校验,主要是为了提高用户体验,例如用户输入一个邮箱地址,要校验这个邮箱地址是否合法,没有必要发送到服务端进行校验,直接在前端用 js 进行校验即可。

但是大家需要明白的是,前端校验无法代替后端校验,前端校验可以有效的提高用户体验,但是无法确保数据完整性,因为在 B/S 架构中,用户可以方便的拿到请求地址,然后直接发送请求,传递非法参数。

2、后端校验

后端必须做参数校验,后端必须做参数校验,后端必须做参数校验,重要的事情表达三次。

数据在网络传输过程中有可能被篡改了,或者数据不满足业务需求,假如不做后端参数校验,则有可能导致系统异常,或者业务出现重大事故。

比如在 SpringBoot 项目中,我们可以使用 hibernate-validator 进行参数校验 。

POST、PUT 请求一般会使用 requestBody 传递参数,这种情况下,后端使用 DTO 对象进行接收。

只要给 DTO 对象加上 @Validated 注解就能实现自动参数校验。比如,有一个保存 User 的接口,要求 userName 长度是 2-10,account 和 password 字段长度

是 6-20。

在 DTO 字段上声明约束注解:

在方法参数上声明校验注解:

虽然,我们可以使用接口校验,可以保证动态 SQL 的参数正确,但是假如我们仅仅只是复用 SQLMap (Dao 方法)时,也有可能因为调用方传递参数错误,导致非预期的问题。

当然,我们也可以使用 Mybatis 拦截器 从根本上来解决,但是我想这样会加大系统的复杂度。于是,我思考了了另外一点:复用和专用要做平衡

4 复用和专用要做平衡

我当时写的那个接口 getUserByConditions ,是支持四种不同参数的查询,同样也是为了省时间,快点出活。

后来,随着我工作经验的日益丰富,我的编程习惯也慢慢发生了改变,对于业务需求明确的场景,我更多的倾向于将通用接口拆分成专用接口。

比如 getUserByConditions 可以拆分成如下四个接口 ,

  • 按照用户 ID 查询用户信息

  • 按照用户昵称查询用户信息

  • 按照手机号查询用户信息

  • 按照用户名查询用户信息

比如按照用户 ID 查询用户信息 , SQLMAP 就简化为:

通过这样的拆分,我们的接口设计更加细粒度,也更容易维护 , 同时也可以规避 where 1 =1 产生的不确定性(虽然我做了后端校验,依然存在不确定性)。

有的同学会有疑问:假如拆分得太细,会不会增加我编写接口和 SQLMap 的工作量 ?

笔者的思路是:定制自己的代码生成器,将生成的 SQLMap 、Mapper 保证更细的颗粒度。

5 防御性编程意识

笔者刚入行的时候,只是机械性的完成任务,并没有思考代码后面的资源占用,以及有没有可能产生恶劣的影响。

随着见识更多的系统,学习开源项目,笔者慢慢培养了一种习惯:

  • 这段代码会占用多少系统资源

  • 如何规避风险 ,做好预防性编程。

其实,这和玩游戏差不多 ,在玩游戏的时,我们经常说一个词,那就是意识。

上图,后裔跟墨子在压对面马可蔡文姬,看到小地图中路铠跟小乔的视野,方向是往下路来的,这时候我们就得到了一个信息。

知道对面的人要来抓,或者是协防,这种情况我们只有两个人,其他的队友都不在,只能选择避战,强打只会损失两名"大将"。

通过小地图的信息,并且想出应对方法,就是叫做"猜测意识"。

编程也是一样的,我们思考代码可能产生的系统资源占用,以及可能存在的风险,并做好防御性编程,就是编程的意识

6 写到最后

人生第一次线上 OOM 事故,因我在使用 Mybatis 动态 SQL 时,没有做后端校验而出现,造成了比较坏的影响。

在后面的职业生涯里面,为了规避生产环境的事故,我试着打磨自己的编程思维,比如做好后端校验平衡好复用和专用接口培养防御性编程的意识

絮絮叨叨这么多,大家可能觉得我小题大做了,但事实是,类似我这样的事故层出不穷,上周我又目睹了一起。

IT 世界有了那么多框架,好像我们依然写不好代码,我有点沮丧。

相关推荐
Leo.yuan40 分钟前
数据量大Excel卡顿严重?选对报表工具提高10倍效率
数据库·数据分析·数据可视化·powerbi
Runing_WoNiu1 小时前
MySQL与Oracle对比及区别
数据库·mysql·oracle
天道有情战天下1 小时前
mysql锁机制详解
数据库·mysql
看山还是山,看水还是。1 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
谷新龙0011 小时前
Redis运行时的10大重要指标
数据库·redis·缓存
CodingBrother1 小时前
MySQL 中单列索引与联合索引分析
数据库·mysql
精进攻城狮@1 小时前
Redis缓存雪崩、缓存击穿、缓存穿透
数据库·redis·缓存
小酋仍在学习1 小时前
光驱验证 MD5 校验和
数据库·postgresql
keep__go2 小时前
Linux 批量配置互信
linux·运维·服务器·数据库·shell
小王同学mf2 小时前
怎么尽可能保证 Kafka 的可靠性
数据库