以前的项目中很少去思考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. API接口安全加强设计方法

    前面两篇相关文章: <Web Api 内部数据思考 和 利用http缓存优化 Api> <Web Api 端点设计 与 Oauth> 1.开放的接口 这样的接口我们天天都在接触 ...

  2. [Scala随用随学] —— sealed声明的作用

    今天在看MLlib的源码时,看到Vector的声明是sealed trait,很好奇这个sealed有什么作用,与是搜到了这个文章: http://www.cnblogs.com/rollenholt ...

  3. django入门-测试-part5

    尊重作者的劳动,转载请注明作者及原文地址 http://www.cnblogs.com/txwsqk/p/6515996.html 完全翻译自官方文档 https://docs.djangoproje ...

  4. nginx实现动静分离--附nginx配置文件详解

    转自http://www.cnblogs.com/1214804270hacker/p/9299462.html 一.认识访问静态资源与访问动态资源的区别 静态资源:指存储在硬盘内的数据,固定的数据, ...

  5. springboot与swagger2的集成

    springboot与swagger2的集成 1.出现的背景 随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染变成了:前端渲染.先后端分离的形态,而前端和后端的唯一联系,变成了API接口: ...

  6. golang (5) http 请求分析

    http 分析包分析 fmt.Println("get Content-Type: ", r.Header.Get("Content-Type")) var r ...

  7. HLS:OpenCV和RTL代码转换关系

    OpenCV 图像处理是基于存储器帧缓存而构建的, 它总是假设视频帧数据存放在外部 DDR 存储器中. 由于处理器的小容量高速缓存性能的限制, 因此, OpenCV 访问局部图像性能较差. 并且, 从 ...

  8. Neo4j安装&入门&一些优缺点(转)

    本篇将介绍Neo4j的安装,入门,和自己使用了一段时间后发现的优点缺点,争取简洁和实用. 如果你是第一次接触Neo4j,并且之前也都没接触过类似的Graph Database的话,建议先浏览一下我之前 ...

  9. mono 的System.Data.SqlClient小记录

    厦门-JuzzPig()  15:33:36System.Data.SqlClient 不科学的广州-PC286()  15:33:42webservice 返回的是 xml厦门-JuzzPig()  ...

  10. CentOS 7进入救援模式的方法

    CentOS 7版本进入救援模式并修改密码:方法1: runlevel 显示当前的运行级别(进入救援模式需要进入单用户模式) 方法2: ①.开机时随便按下键盘,进入系统选择菜单 ②.选择第一项,按e键 ...