以前的项目中很少去思考SQL解析这个事情,即使在saas系统或者分库分表的时候有涉及到也会有专门的处理方案,这些方案也对使用者隐藏了实现细节。

而最近的这个数据项目里面却频繁涉及到了对SQL的处理,原来只是简单地了解Druid的SqlParser模块就可以解决,慢慢地问题变得越来越复杂,直到某天改动自己写的SQL处理的代码很痛苦的时候,意识到似乎有必要更加地了解一下相关的内容才行。

在了解学习的过程中,发现学习使用SqlParser还是得先了解ast(抽象语法树)这个概念,一搜索相关内容要么是编译原理相关的知识,要么是JavaScript的示例,光看Druid提供的SqlParser相关的Wiki文档又似懂非懂,不知道从哪里下手。

不管怎么样,看了不少碎片化的相关内容以后也收获了一些东西,这里记录下来。

为什么要先了解ast?

ast全称是abstract syntax tree,中文直译抽象语法树。

原先我觉得要使用SqlParser就照着wiki上的代码步骤拷过来就好了呗,也确实如此,它快速解决了我的问题。可是正如上面所说,希望你的相关代码写得更好一点,或者更理解它是在干吗了解了ast会有不少的帮助。

SQL解析,本质上就是把SQL字符串给解析成ast,也就是说SqlParser的入参是SQL字符串,结果就是一个ast。你怎么使用这个ast结果又是另外一回事,你可以修改ast,也可以添加点东西等等,但整个过程都是围绕着ast这个东西。

什么是ast?

上面提了好几次ast,那ast又是个什么东西呢?

参照维基百科的说法,在计算机科学领域内,ast表示的是你写的编程语言源代码的抽象语法结构。如图:

左边是一个非常简单的编程语言源代码:1 + 2,做了一个加法计算,而当它被解析成ast以后如右边的图所示。我们可以看到ast存在三个节点,顶部的 + 表示一个加法节点,这个表达式组合了1、2两个数值节点,由这三个组合在一起的节点就组成了1+2这样的语法结构。

我们看到ast很清晰地用数据结构表示出了字符串源代码,ast的每一个节点均表示源代码当中的一个语法结构。反过来思考一下,我们可以知道源代码解析出来的ast是由很多这样简单的语法结构组合而成的,也就形成了一个复杂的语法树。下面我们看一个稍微复杂一点的,来自维基百科的示例

源代码:

 while b ≠ 0
if a > b
a = a − b
else
b = b − a
return a

语法树:

这个语法树也清晰地表示的源代码程序,主要由一个while语法和if/else语法以及一些变量之类的组成。

到这里,似乎对源代码和ast有了一个简单的概念,但是还是存在困惑,我为什么要把好好的代码搞成这样?它有什么用?如果只是修改语法,我用正则表达式修改字符串不是简单吗?

确实,有的时候直接处理字符串会是更快速更好的解决方式,但是当源程序语法非常复杂的时候字符串处理的复杂度已经不是一个简单的事了。而ast则把这些字符串变成结构化的数据了,你可以精确地知道一段代码里面有哪些变量名,函数名,参数等,你可以非常精准地处理,相对于字符串处理来说,遍历数据大大降低的处理难度。而ast也常常用在如IDE中错误提示、自动补全、编译器、语法翻译、重构、代码混淆压缩转换等。

了解ast可以参考文章:

https://mp.weixin.qq.com/s/UYzwVRPFas6hwe2U7R0eIg

https://www.cnblogs.com/jacksplwxy/p/10676578.html

https://blog.csdn.net/fei33423/article/details/79452922

https://www.jianshu.com/p/6a2f4ae4e099

https://en.wikipedia.org/wiki/Abstract_syntax_tree

https://en.wikipedia.org/wiki/Parse_tree#Constituency-based_parse_trees

SqlParser

我们知道了ast是一种结构化的源代码表示,那针对SQL来说ast就是把SQL语句用结构化的数据来表示了。而SqlParser也就是把SQL解析成ast,这个解析过程则被SqlParser做了隐藏,我们不需要去实现这样一个字符串解析过程。

由此可见,我们需要了解两方面内容:

1、怎么用SqlParser把SQL语句解析成ast;

2、SqlParser解析出来的ast是什么样的一个结构。

下面需要一点代码来说明,所以先引入一下maven依赖

 <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>

解析成ast

解析语句相对简单,wiki上直接有示例,如:

String dbType = JdbcConstants.MYSQL;
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, dbType);

SQLUtils的parseStatements方法会把你传入的SQL语句给解析成SQLStatement对象集合,每一个SQLStatement代表一条完整的SQL语句,如:

SELECT id FROM user WHERE status = 1

多个SQLStatement如:

SELECT id FROM user WHERE status = 1;
SELECT id FROM order WHERE create_time > '2018-01-01'

一般上我们只处理一条语句。

ast的结构

SQLStatement表示一条SQL语句,我们知道常见的SQL语句有CRUD四种操作,所以SQLStatement会有四种主要实现类,如:

class SQLSelectStatement implements SQLStatement {
SQLSelect select;
}
class SQLUpdateStatement implements SQLStatement {
SQLExprTableSource tableSource;
List<SQLUpdateSetItem> items;
SQLExpr where;
}
class SQLDeleteStatement implements SQLStatement {
SQLTableSource tableSource;
SQLExpr where;
}
class SQLInsertStatement implements SQLStatement {
SQLExprTableSource tableSource;
List<SQLExpr> columns;
SQLSelect query;
}

这里我们以SQLSelectStatement来说明,ast既然是SQL的语法结构表示,我们先看一下ast和SQL select语法的主要对应结构

SQLSelectStatement包含一个SQLSelect,SQLSelect包含一个SQLSelectQuery,都是组成的关系。SQLSelectQuery有主要的两个派生类,分别是SQLSelectQueryBlock和SQLUnionQuery。

class SQLSelect extends SQLObjectImpl {
SQLWithSubqueryClause withSubQuery;
SQLSelectQuery query;
} interface SQLSelectQuery extends SQLObject {} class SQLSelectQueryBlock implements SQLSelectQuery {
List<SQLSelectItem> selectList;
SQLTableSource from;
SQLExpr where;
SQLSelectGroupByClause groupBy;
SQLOrderBy orderBy;
SQLLimit limit;
} class SQLUnionQuery implements SQLSelectQuery {
SQLSelectQuery left;
SQLSelectQuery right;
SQLUnionOperator operator; // UNION/UNION_ALL/MINUS/INTERSECT
}

以下是SQLSelectQueryBlock中包含的主要节点

sql ast
字段 SQLSelectItems
SQLTableSource
where条件 SQLExpr
groupby SQLSelectGroupByClause
orderby SQLOrderBy
limit SQLLimit

表格中的这些ast节点都是SQL对应语法的一些表示,相信大家都非常熟悉,根据名字也轻易能了解具体是语法。

这里需要细化一下SQLTableSource这个节点,它有着常见的实现SQLExprTableSource(from的表)、SQLJoinTableSource(join的表)、SQLSubqueryTableSource(子查询的表)如:

class SQLTableSourceImpl extends SQLObjectImpl implements SQLTableSource {
String alias;
} // 例如 select * from emp where i = 3,这里的from emp是一个SQLExprTableSource
// 其中expr是一个name=emp的SQLIdentifierExpr
class SQLExprTableSource extends SQLTableSourceImpl {
SQLExpr expr;
} // 例如 select * from emp e inner join org o on e.org_id = o.id
// 其中left 'emp e' 是一个SQLExprTableSource,right 'org o'也是一个SQLExprTableSource
// condition 'e.org_id = o.id'是一个SQLBinaryOpExpr
class SQLJoinTableSource extends SQLTableSourceImpl {
SQLTableSource left;
SQLTableSource right;
JoinType joinType; // INNER_JOIN/CROSS_JOIN/LEFT_OUTER_JOIN/RIGHT_OUTER_JOIN/...
SQLExpr condition;
} // 例如 select * from (select * from temp) a,这里第一层from(...)是一个SQLSubqueryTableSource
SQLSubqueryTableSource extends SQLTableSourceImpl {
SQLSelect select;
}

另外SQLExpr出现的地方也比较多,比如where语句,join条件,SQLSelectItem中等,因此,也需要细化了解一下,如

// SQLName是一种的SQLExpr的Expr,包括SQLIdentifierExpr、SQLPropertyExpr等
public interface SQLName extends SQLExpr {} // 例如 ID = 3 这里的ID是一个SQLIdentifierExpr
class SQLIdentifierExpr implements SQLExpr, SQLName {
String name;
} // 例如 A.ID = 3 这里的A.ID是一个SQLPropertyExpr
class SQLPropertyExpr implements SQLExpr, SQLName {
SQLExpr owner;
String name;
} // 例如 ID = 3 这是一个SQLBinaryOpExpr
// left是ID (SQLIdentifierExpr)
// right是3 (SQLIntegerExpr)
class SQLBinaryOpExpr implements SQLExpr {
SQLExpr left;
SQLExpr right;
SQLBinaryOperator operator;
} // 例如 select * from where id = ?,这里的?是一个SQLVariantRefExpr,name是'?'
class SQLVariantRefExpr extends SQLExprImpl {
String name;
} // 例如 ID = 3 这里的3是一个SQLIntegerExpr
public class SQLIntegerExpr extends SQLNumericLiteralExpr implements SQLValuableExpr {
Number number; // 所有实现了SQLValuableExpr接口的SQLExpr都可以直接调用这个方法求值
@Override
public Object getValue() {
return this.number;
}
} // 例如 NAME = 'jobs' 这里的'jobs'是一个SQLCharExpr
public class SQLCharExpr extends SQLTextLiteralExpr implements SQLValuableExpr{
String text;
}

SqlParser定义了完整的ast各个节点对象,一条SQL语句被解析成这些对象的树形结构,而我们要做的就是根据这样的一个树形结构去做相应的处理。以上代码片段摘取了部分wiki上的,并调整了一下顺序,完整的wiki可以到druid的github上查阅。

使用示例

public void enhanceSql(String sql) {
// 解析
List<SQLStatement> statements = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL);
// 只考虑一条语句
SQLStatement statement = statements.get(0);
// 只考虑查询语句
SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) statement;
SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery();
// 非union的查询语句
if (sqlSelectQuery instanceof SQLSelectQueryBlock) {
SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery;
// 获取字段列表
List<SQLSelectItem> selectItems = sqlSelectQueryBlock.getSelectList();
selectItems.forEach(x -> {
// 处理---------------------
});
// 获取表
SQLTableSource table = sqlSelectQueryBlock.getFrom();
// 普通单表
if (table instanceof SQLExprTableSource) {
// 处理---------------------
// join多表
} else if (table instanceof SQLJoinTableSource) {
// 处理---------------------
// 子查询作为表
} else if (table instanceof SQLSubqueryTableSource) {
// 处理---------------------
}
// 获取where条件
SQLExpr where = sqlSelectQueryBlock.getWhere();
// 如果是二元表达式
if (where instanceof SQLBinaryOpExpr) {
SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) where;
SQLExpr left = sqlBinaryOpExpr.getLeft();
SQLBinaryOperator operator = sqlBinaryOpExpr.getOperator();
SQLExpr right = sqlBinaryOpExpr.getRight();
// 处理---------------------
// 如果是子查询
} else if (where instanceof SQLInSubQueryExpr) {
SQLInSubQueryExpr sqlInSubQueryExpr = (SQLInSubQueryExpr) where;
// 处理---------------------
}
// 获取分组
SQLSelectGroupByClause groupBy = sqlSelectQueryBlock.getGroupBy();
// 处理---------------------
// 获取排序
SQLOrderBy orderBy = sqlSelectQueryBlock.getOrderBy();
// 处理---------------------
// 获取分页
SQLLimit limit = sqlSelectQueryBlock.getLimit();
// 处理---------------------
// union的查询语句
} else if (sqlSelectQuery instanceof SQLUnionQuery) {
// 处理---------------------
}
}

以上示例中只是简单判断了一下类型,实际项目中你可能需要对整个ast做递归之类的方式来处理节点。其实当SQL语句变成了ast结构以后,我们只要知道这个ast结构存在什么样的节点,获取节点判断类型并做相应的操作即可,至于你是递归还是访问者模式还是别的什么方式去处理ast都可以。

SqlParser官方wiki

抽象语法树: https://github.com/alibaba/druid/wiki/Druid_SQL_AST#24-sqlselect--sqlselectquery

解析器:https://github.com/alibaba/druid/wiki/SQL-Parser

visitor访问者示例:https://github.com/alibaba/druid/wiki/SQL_Parser_Demo_visitor

Druid SqlParser理解及使用入门的更多相关文章

  1. (转载)从Java角度理解Angular之入门篇:npm, yarn, Angular CLI

    本系列从Java程序员的角度,带大家理解前端Angular框架. 本文是入门篇.笔者认为亲自动手写代码做实验,是最有效最扎实的学习途径,而搭建开发环境是学习一门新技术最需要先学会的技能,是入门的前提. ...

  2. 从Java角度理解Angular之入门篇:npm, yarn, Angular CLI

    本系列从Java程序员的角度,带大家理解前端Angular框架. 本文重点介绍Angular的开发.编译工具:npm, yarn, Angular CLI,它们就像Java在中的Maven,同时顺便介 ...

  3. druid sqlparser使用例子

    package com.alibaba.druid.bvt.sql.mysql; import java.util.List; import org.junit.Assert; import com. ...

  4. 必须先理解的RocketMQ入门手册,才能再次深入解读

    RocketMQ入门手册 RocketMQ是一个分布式.队列模型的开源消息中间件,前身是MetaQ,是阿里研发的一个队列模型的消息中间件,后开源给apache基金会成为了apache的顶级开源项目,具 ...

  5. SAP模块一句话入门(专业术语的理解)

    SAP模块一句话入门(专业术语的理解) SAP一句话入门:Financial & Controlling Accounting (FICO) 财务,财务,呵呵,让我们关心一下给我发工资的部门. ...

  6. Druid入门(1)—— 快速入门实时分析利器-Druid_0.17

    一.安装准备 本次安装的版本是截止2020.1.30最新的版本0.17.0 软件要求 需要Java 8(8u92 +)以上的版本,否则会有问题 Linux,Mac OS X或其他类似Unix的操作系统 ...

  7. Druid 0.17 入门(2)—— 安装与部署

    在Druid快速入门其实已经简单的介绍过最简化配置的单节点部署,本文我们将详细描述Druid的多种部署方式,对于测试开发环境可以选用轻量的单机部署方式,而生产环境我们最好选用集群部署的方式,确保系统的 ...

  8. 源码详解系列(六) ------ 全面讲解druid的使用和源码

    简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...

  9. [转]oracle学习入门系列之五内存结构、数据库结构、进程

    原文地址:http://www.2cto.com/database/201505/399285.html 1 Oracle数据库结构 关于这个话题,网上一搜绝对一大把,更别提书籍上出现的了,还有很多大 ...

随机推荐

  1. ADB模块源码分析(二)——adb server的启动

    1. ADB Server的启动 前面我们讲到adb模块的源码在system/core/adb下面,通过查看Android.mk文件我们了解到这个adb 模块回编译生成连个可执行文件adb.adbd, ...

  2. 【OCP-12c】CUUG 071题库考试原题及答案解析(15)

    15.(6-24)choose the best answerExamine the structure of the MEMBERS table:You want to display detail ...

  3. IE8兼容性问题 val()

    IE8有时使用val()不正常,需要使用attr('value') 另外还有一些其他问题: https://blog.csdn.net/chenyiminnanjing/article/details ...

  4. CodeChef April Challenge 2019题解

    传送门 \(Maximum\ Remaining\) 对于两个数\(a,b\),如果\(a=b\)没贡献,所以不妨假设\(a<b\),有\(a\%b=a\),而\(b\%a<a\).综上, ...

  5. memcache面试题汇总

    1,memcached是原子的吗? 所有的被发送到memcached的单个命令是完全原子的.如果您针对同一份数据同时发送了一个set命令和一个get命令,它们不会影响对方.它们将被串行化.先后执行.即 ...

  6. 【APUE】第3章 文件I/O (1)

    1.文件描述符 对于内核来说,所有打开的文件都通过文件描述符来引用.文件描述符是一个非负整数.当打开一个现有的文件或者创建一个新文件时,内核向进程返回一个文件描述符.当读.写一个文件时,使用open或 ...

  7. flask _bootstrap中使用flash

    在模板中获取flash闪现的那段代码要和内容块放在同一级别上.不然网页上是看不到闪现的内容的. 比如在基模板里定义一个content block ,里面一个是get_flashed_messages代 ...

  8. 【Vue】环境搭建、项目创建及运行

    一.软件下载 1. 进入官网https://nodejs.org/en/下周node.js,傻瓜式安装步骤(一直下一步就好) 2. 进入官网http://www.dcloud.io/下载并安装编辑器H ...

  9. 南昌 Max answer

    https://nanti.jisuanke.com/t/38228 Alice has a magic array. She suggests that the value of a interva ...

  10. Vue.js 动态挂载子组件

    直接来实例 父组件 <template> <div> <div :is="sub.content" :ref="sub.name" ...