原计划 7 天的工作,1 小时完成!是我开挂了么?
大家好,我是鱼皮,今天分享自己工作中的小欢喜,也希望给大家带来一些编程上的思考。
孽起
事情是这样的,最近在开发一个 仅限内部使用 的数据分析系统,我做后端,另外一个哥们做前端。
我们要实现的功能是:用户可以在界面上任意输入 SQL 数据查询语句,并将它保存下来,生成一个数据看板。以后用户可以随时打开这个看板来浏览和分析 SQL 查询出的最新数据,而无需反复输入 SQL 语句。
举个例子!
假如我们有一个很大的数据仓库,存了海量的数据,有男有女:
产品同学可能只想对部分数据进行分析,于是写了下列 SQL 语句来查询所有男性:
select * from table
where 性别 = '男';
将该 SQL 语句保存,得要一个 “男性数据看板”,之后,就可以在该看板页面查看和分析所有男性数据啦。
数据看板
要实现这个需求,一种最简单的方式就是,直接将用户在界面上输入的 SQL 字符串发给后端保存,需要看数据时,后端再用这个字符串从数据库中查询数据即可。
写 SQL 配置流程:
打开看板浏览数据流程:
既然是允许用户任意输入的,那么问题就来了。
假如小粗心不小心打错了 SQL 语句:
# 错误 ❌
sleetc * from table
# 正确 ✅
select * from table;
又或者小迷糊记错了 SQL 的语法:
# 错误 ❌
select table from a;
# 正确 ✅
select a from table;
甚至是小捣蛋不按规矩出牌,输入一些乱七八糟的字符:
# 错误 ❌
select q^q from table;
# 正确 ✅
select q from table;
如果把这些错误的 SQL 语句发给后端,后端直接用它来查数据库,必然会导致查询错误,查了个寂寞。
对于实时查询来说,这没啥问题,查询失败了大不了再修改语句查询一次呗。
但我要做的需求是允许用户将查询语句作为看板配置永久保存下来,便于后续自动查询数据。而且写 SQL 配置的用户可能和看数据的用户不是同一个人,如果小 A 在配置时就没有发现 SQL 语句是错误的,那到时候来查看数据看板的小 B 就会一脸懵逼,咋特么看不到数据呢?是数据还没准备好,还是查询出来的数据就是 0 行呢,还是说我没有浏览权限呢?
他根本不会想到,已经配置成功的 SQL 语句,竟然是错误的!
因此,需要在配置时就对用户输入的 SQL 进行校验,看看它是否合法。
做个比喻,前端是一名底层员工(无知的小开发),后端是小组长,数据库是大老板。小开发做了个需求之后,应该先交给小组长检查,小组长说没问题之后,再给大老板验收。
那如何校验 SQL 语句呢?
因为用户的输入是完全不确定的,他们写的 SQL 语句可能又臭又长。所以我刚想到这个需求,就觉得脑阔疼,感觉贼麻烦,不保守地给自己计划 7 天完成。
大家可以先想想如果让你实现 SQL 语句校验,你会怎么做?
下面是我的思考过程。
绞尽脑汁
首先,我们要明确:是在前端,还是在后端校验?
其实,无论在前端还是后端,校验都至关重要,可以有效防止很多错误的输入。但由于最终是后端程序来直接操作数据库,可以说是数据库的最后一道防线,因此建议 将校验逻辑写在后端。数据库很嫩,他自己把握不住,需要后端程序来帮他把握把握。
那如何在后端去校验 SQL 呢?
找现成的
首先,遇事不决问百度,不行再去搜仓库。现在网上的开源项目很多,那不妨搜搜看,有没有现成的 SQL 校验类库。最理想的情况是,有一个工具类函数,我传给他 SQL 字符串作为参数,他直接返回给我 true 或 false。
然而,我发现自己在想 peach,各种开源项目都搜遍了,没有找到能开箱即用的 PostgreSQL 校验库。
看来,只能自己动手,丰衣足食了。
模拟查询
要自己实现校验,我第一时间想到的方法是模拟一次查询。用户刚刚写好 SQL 语句后,即便他现在并不需要浏览数据查询结果,我也可以在他保存配置时,用他写的 SQL 去查询一次数据库。假如查询没报错,就说明 SQL 语句合法,允许保存。
这种方式最直接,也最方便,基本没有任何的开发成本,贼香!就好比一名小开发写完烂代码后,交给小组长,但小组长不讲武德,自己看不懂代码(也可能是不想看),索性就把代码直接丢给大老板,大老板说没问题了,小开发再上线。小组长狂喜!
但是,有个致命的问题:用户在配置 SQL 语句时,数据表可能还没准备好,无论语句是否正确,都无法查出数据。
所以,在将 SQL 语句直接发向数据库前,要先确认数据表是否存在。若存在,可以通过模拟查询的方式校验;若不存在,只能在后端通过其他方式校验。
就好比小组长想把烂代码直接丢给大老板时,大老板不在,这时,只能靠自己来检查了。
正则表达式
要在程序中校验字符串,我最先想到的是 正则表达式,即用特定语法来匹配同一类具有相似规则的字符串,常见的有校验手机号、校验邮箱、校验身份证等。
在使用正则表达式进行校验前,我们要先对字符串进行分析,看它们是否具有相似的结构、哪些部分相似。比如 QQ 邮箱,结构很规整,基本都是 xxx@qq.com
,因此,可以用正则表达式 /^\w+@qq.com$/
来校验。
回过头来看我们的需求,要校验的是 SQL 语句,似乎也比较规整,无非就是查询哪个表、选哪些行、选哪些列、怎么排序等等,大概的结构是这样:
SELECT select_list
[ INTO new_table ]
FROM table_source
[ WHERE search_condition ]
[ GROUP BY group_by_expression ]
[ HAVING search_condition ]
[ ORDER BY order_expression [ ASC | DESC ] ]
根据这个结构,很容易编写出粗略的正则表达式。但是,数据业务中的 SQL 语句可比这复杂得多,包含各种四则运算、IF ... ELSE 条件判断、CASE ... WHEN ... 分支,字符串、日期类型处理函数,还有各种聚合函数等,比如下面这个 SQL:
select a as b,
sum(case when (false) then d / a else 2 end) as c
from table
where a = 1
group by b, c;
如果以上这些零碎的语法都用正则表达式来匹配,可就太麻烦了!想想脑阔又疼了。
解析表达式
既然编写一套正则表达式比较麻烦,那我能想到的就只有把 SQL 打的稀吧碎了。可以用类似编译原理语法分析的方式,搞一个 SQL 解析器,将完整的 SQL 语句转换为一颗抽象语法树(AST),每个节点都是一个小表达式,从而能够更精细地校验 SQL 语句的合法性。
SQL 表达式抽象语法树
如果自己从零开始实现这样一套 SQL 解析器,实在是太麻烦了,而且不具备一定的专业知识也写不出来。因此,我先到网上去搜索一番,看看有没有现成的解析器引擎。
这次的搜索结果还算满意,找到了一些知名解析引擎,但是看了一圈,读了半天,发现很难直接去使用他们的源码。那委曲求全的方式就是照着他们的源码自己写一个解析器了。
想到这里,头顶不仅感受到了一丝寒凉,感觉给自己估时 7 天都少了。
移花接木
第二天,我又思考了一下,网上有那么多现成的类库,难道就没有一个能满足我的需求?即使没有完全现成的,能不能找个相对好用的呢?
毕竟自己来写这复杂的校验逻辑实在太麻烦了,所以我必须再挣扎一下!
于是,我掏出了御用小黄鸭,开始对着它念叨:SQL 校验、SQL 校验、SQL 校验。。。
我:什么时候会用到 SQL 校验呢?
小黄鸭:需要查数据库的时候。
我:什么东西会去查数据库呢?
小黄鸭:框架、数据库连接池、或者代理。
我:那这些玩意在查数据库的时候,会帮我们做校验么?
小黄鸭:校验校验,你就知道校验,你需要的功能一定是校验么?
等等,我好像恍然大悟了!
既然没办法直接搜到现成的 SQL 校验类库,那不妨来个 移花接木,想一想其他的类库中是否包含 SQL 解析功能,如果解析失败,不就表示 SQL 非法,校验不通过么!
我开始回想自己以前用过的和访问数据库有关的技术,突然想到,阿里的 Druid 数据库连接池类库好像有一个 SQL 语句格式化的功能,能把杂乱的 SQL 重新排版。既然能对 SQL 格式化,是不是意味着,这个类库有能力对 SQL 语句进行解析呢?
仔细一查 Druid 的文档,发现还真有一个类叫 SQLUtils
,这个类有一个方法叫 parseStatements
,可以对多种不同的 SQL 方言进行解析,比如 MySQL、PostgreSQL 等。
// 解析,接受 sql 语句和数据库方言为参数
SQLUtils.parseStatements(sql, POSTGRESQL);
解析失败时,会抛出异常,表示 SQL 语句非法,正好能够满足我的需求!
最终,我写出的代码如下:
try {
String sql = "select * from a";
SQLUtils.parseStatements(sql, POSTGRESQL);
return true;
} catch (ParserException e) {
LOGGER.error("解析失败", e);
return false;
}
几分钟就写完了代码,然后又花了一些时间输入各种 SQL 语句来测试,虽然只能实现基本的语法校验,但综合衡量效果和成本上,我觉得已经不错了,省下的大量时间可以继续完善和优化项目的其他代码。
关键是,心不累了,头发又支棱起来了!
通过这件事,带给我三点思考:
-
在我们找项目代码、找类库的时候,如果没办法找到直接满足需求的,那么可以把思维从整体转向局部,想想在其他的项目中是否包含了你要找的功能。就像查词典一样,你要查单词
apple
,但是翻目录只有首字母a
,这个时候,就不能只盯着a
看,而是要看到词典里面的内容,其实apple
就藏在a
之中。 -
前人栽树,后人乘凉,现在网上现成的项目代码太多了,如果不是为了学习,很多东西没必要自己再去实现一遍。
-
写代码时要注重积累,多学习和了解技术,并归纳总结到你的武器库中,否则前人栽的树你找不到,就可惜了。
当然,有条件的话,前端也是可以加校验的,但目前没啥必要,这里我们先用 CodeMirror
做一个 SQL 代码高亮来替代。
如果真的让你实现前端 SQL 校验,你会怎么做呢?
我是鱼皮,原创不易,如果觉得文章还不错的话,希望朋友们 点赞 + 在看 支持下,给俺点创作动力。
https://mp.weixin.qq.com/s/2bP8XVHO5x3SAZTcwGELFA