网站首页 文章专栏 深入druid之sqlparse,sql语法是如何被解析的
druid是alibaba在github上开源的项目,它是一个组件库,包含了数据库连接池、Sql parser等组件。
DruidDataSource号称是Java语言中最全面最好的连接池,理论上支持所有的JDBC驱动的数据源(实际测试并未把所有数据源测试一遍)。我们对他的了解也更多地是在如何将它作为连接池应用在程序中,实际它还有其他的组成。
github地址:https://github.com/alibaba/druid
可能我们在学Mysql的时候会想过,sql语法是怎么解析呢,执行的时候是怎么知道sql语法是正确的呢?当我们编写一个巨长巨复杂的sql时,也会想这个执行是按照什么顺序的呢?其实我之前在面试京东的时候,面试官也问过mysql的解析器了解过吗,是怎么解析sql的,当时的我大概也就只知道是有词法分析,语法分析,具体怎么实现的实际并不知道。
druid中的SQL parser是druid的重要组成部分,SQL parser在druid中承担着重要责任,如:SQL解析、防御SQL注入、格式化、分库分表等。sqlParse实际就是一套对sql语法的解析,我们利用它就可以分析sql是否正确,从sql中获取表名,列名,where条件等,也可以在sql基础上添加一些语法。
Druid SQL Parser分三个模块:
- Parser
- AST
- Visitor
parser是将输入文本转换为ast(抽象语法树),parser有包括两个部分,Parser和Lexer,其中Lexer实现词法分析,Parser实现语法分析。
AST是Abstract Syntax Tree的缩写,也就是抽象语法树。AST是parser输出的结果。这也是语法树的精髓了,sql解析,本质上就是把sql转为 ast语法树,拿到这个语法树后,我们就能做很多事了,遍历也好,加点,修改也好,都可以在ast上完成。
怎么理解ast呢?
就拿基本的数学运算来说,1+1=2,就可以用下面的图表示
可以看到ast存在三个节点,顶部的 + 表示一个加法节点,这个表达式组合了1、2两个数值节点,由这三个组合在一起的节点就组成了1+1这样的语法结构,那么再复杂的逻辑怎么处理呢?比如4 / (1+1)* 2这个表达式。
通过多个condition,以及左右叶子节点,就能构建足够复杂的语法树。那么为什么需要这么构建呢,当我们语法很简单时,那么无所谓,直接匹配字符串,或者正则也行,但是当语法足够复杂时,简单的匹配字符串,正则就不够用了,你很难检测语法是否正确,而且想要在此基础上增加语法,也几乎很难做到。而ast则把这些字符串变成结构化的数据了,你可以精确地知道一段代码里面有哪些变量名,函数名,组合条件,参数等,你可以非常精准地处理,相对于字符串处理来说,遍历数据大大降低的处理难度。而ast也常常用在如IDE中错误提示、自动补全、编译器、语法翻译、重构、代码混淆压缩转换等。
Visitor是遍历AST的手段,是处理AST最方便的模式,Visitor是一个接口,有缺省什么都没做的实现VistorAdapter。
我们可以实现不同的Visitor来满足不同的需求,Druid内置提供了如下Visitor:
- OutputVisitor用来把AST输出为字符串
- OutputVisitor用来把AST输出为字符串
- WallVisitor 来分析SQL语意来防御SQL注入攻击
- ParameterizedOutputVisitor用来合并未参数化的SQL进行统计
- EvalVisitor 用来对SQL表达式求值
- ExportParameterVisitor用来提取SQL中的变量参数
- SchemaStatVisitor 用来统计SQL中使用的表、字段、过滤条件、排序表达式、分组表达式
- SQL格式化 Druid内置了基于语义的SQL格式化功能
有语法树了,那么怎么从中获取想要的节点,数据呢,看到这个树,基本我们能想到二叉树的遍历,如果自己去遍历的话,是一件很费时的操作,而druid的sqlParse正好提供了现成的访问,甚至我们可以自定义visitor,来实现我们的查询逻辑。
以上我们大概了解了sqlParse是什么,基本原理,以及能做什么,现在我们来写个demo实践下。
druid是java实现的,我们先引入包
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency>
注意,druid的版本是有差异的,有的版本并不向下兼容,曾经升级过导致线上爆炸。。。
准备一条sql:
SELECT a.id, a.`name`, a.age FROM `user` a LEFT JOIN order_info o ON a.id = o.user_id WHERE a.id = 1 AND a.name = 'xxx' AND a.age = (SELECT age FROM age_table age WHERE age.user_id = 1) GROUP BY a.id, a.score HAVING MAX(a.id) > 1 ORDER BY a.id DESC, a.score ASC LIMIT 3, 10;
不算是复杂的sql,包含join,where,子查询,group by,order by , limit等基本语法,我们看下sqlParse是如何解析的。
String dbType = JdbcConstants.MYSQL; List statementList = SQLUtils.parseStatements(sql, dbType); SQLStatement statement = statementList.get(0); // 只考虑查询语句 SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) statement; SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery(); // 非union的查询语句 if (sqlSelectQuery instanceof SQLSelectQueryBlock) { SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery; // 获取字段列表 List selectItems = sqlSelectQueryBlock.getSelectList(); System.out.println("字段信息:========="); selectItems.forEach(System.out::println); // 获取表 SQLTableSource table = sqlSelectQueryBlock.getFrom(); System.out.println("表信息:========="); if (table instanceof SQLExprTableSource) { // 普通单表 SQLExprTableSource sqlExprTable = (SQLExprTableSource) table; System.out.println("当前为单表,表名为:" + sqlExprTable.getName()); System.out.println("别名为:" + sqlExprTable.getAlias()); } else if (table instanceof SQLJoinTableSource) { SQLJoinTableSource joinTable = (SQLJoinTableSource) table; // join多表 System.out.println("当前为join表,join类型为:" + joinTable.getJoinType()); System.out.println("join条件为:" + joinTable.getCondition()); } else if (table instanceof SQLSubqueryTableSource) { // 子查询作为表 SQLSubqueryTableSource subQueryTable = (SQLSubqueryTableSource) table; System.out.println("参数:" + subQueryTable.getAttributes()); } // 获取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(); System.out.println("where信息:========="); System.out.println("db类型:" + sqlBinaryOpExpr.getDbType()); System.out.println("where字段:" + left.toString()); System.out.println("where类型:" + operator.getName()); System.out.println("where值:" + right.toString()); } else if (where instanceof SQLInSubQueryExpr) { // 如果是子查询 SQLInSubQueryExpr sqlInSubQueryExpr = (SQLInSubQueryExpr) where; // 处理--------------------- } // 获取分组 SQLSelectGroupByClause groupBy = sqlSelectQueryBlock.getGroupBy(); System.out.println("分组信息:========="); groupBy.getItems().forEach(System.out::println); System.out.println("分组条件:" + groupBy.getHaving().toString()); // 获取排序 SQLOrderBy orderBy = sqlSelectQueryBlock.getOrderBy(); System.out.println("排序信息:========="); orderBy.getItems().forEach(o -> { SQLOrderingSpecification type = o.getType(); SQLExpr expr = o.getExpr(); System.out.println("排序字段:" + expr); System.out.println("排序类型:" + type.name()); }); // 获取分页 SQLLimit limit = sqlSelectQueryBlock.getLimit(); System.out.println("分页信息:========="); SQLExpr rowCount = limit.getRowCount(); SQLExpr offset = limit.getOffset(); System.out.println("偏移量:" + offset.toString()); System.out.println("获取数量:" + rowCount.toString()); } else if (sqlSelectQuery instanceof SQLUnionQuery) { // union的查询语句 // 处理--------------------- }
可以看出通过这个直接就返回了一个 SQLStatement对象集合,,每一个SQLStatement代表一条完整的SQL语句,如果我们sql是多条用;分割的话,那么就会是个list,如果只有一条sql,那么list就只有一个元素。
String dbType = JdbcConstants.MYSQL; List statementList = SQLUtils.parseStatements(sql, dbType);
SQLStatement表示一条SQL语句,我们知道常见的SQL语句有CRUD四种操作,所以SQLStatement会有四种主要实现类,如:
class SQLSelectStatement implements SQLStatement { SQLSelect select; } class SQLUpdateStatement implements SQLStatement { SQLExprTableSource tableSource; List items; SQLExpr where; } class SQLDeleteStatement implements SQLStatement { SQLTableSource tableSource; SQLExpr where; } class SQLInsertStatement implements SQLStatement { SQLExprTableSource tableSource; List columns; SQLSelect query; }
我们这条查询时select,所以转为SQLSelectStatement,SQLSelectStatement包含一个SQLSelect,SQLSelect包含一个SQLSelectQuery,都是组成的关系。SQLSelectQuery有主要的两个派生类,分别是SQLSelectQueryBlock和SQLUnionQuery。表示非union的查询和union的查询。
SQLSelectQueryBlock中包含的主要节点:
sql | 对象 | 来源 | 说明 |
---|---|---|---|
字段 | SQLSelectItems | sqlSelectQueryBlock.getSelectList() | 字段的列表 |
表 | SQLTableSource | sqlSelectQueryBlock.getFrom() | sql中的表,分为几种类型,普通单表,join多表,子查询作为的表 |
where条件 | SQLExpr | sqlSelectQueryBlock.getWhere() | 分为普通查询,与子查询,多个where条件通过ast,二叉树的形式向下延申 |
group by | SQLSelectGroupByClause | sqlSelectQueryBlock.getGroupBy() | 包含分组列表,与分组条件信息 |
order by | SQLOrderBy | sqlSelectQueryBlock.getOrderBy() | 包含排序列表,与升降序信息 |
limit | SQLLimit | sqlSelectQueryBlock.getLimit() | 包含偏移量,与页容量 |
这么一拆分的话,我们的sql便成为了如下的一棵树:(点击图片可放大)
通过以上语法树对象,我们便获取了基本的sql信息,看下输出:
字段信息:========= a.id a.`name` a.age 表信息:========= 当前为join表,join类型为:LEFT_OUTER_JOIN join条件为:a.id = o.user_id where信息:========= db类型:mysql where字段:a.id = 1 AND a.name = 'xxx' where类型:AND where值:a.age = ( SELECT age FROM age_table age WHERE age.user_id = 1 ) 分组信息:========= a.id a.score 分组条件:MAX(a.id) > 1 排序信息:========= 排序字段:a.id 排序类型:DESC 排序字段:a.score 排序类型:ASC 分页信息:========= 偏移量:3 获取数量:10
这个是我们自己的查询,如果想系统的获取sql中包含的table name和字段列表的话,可以使用提供的visitor来获取。
SchemaStatVisitor statVisitor = SQLUtils.createSchemaStatVisitor(dbType); for (SQLStatement stmt : statementList) { stmt.accept(statVisitor); } System.out.println("使用visitor数据表:" + statVisitor.getTables()); System.out.println("使用visitor字段:" + statVisitor.getColumns()); for (TableStat.Column column : statVisitor.getColumns()) { if (column.isSelect()){ System.out.println("查询的字段:" + column.getFullName() + "," + column.getName()); } } System.out.println("使用visitor条件:" + statVisitor.getConditions()); System.out.println("使用visitor分组:" + statVisitor.getGroupByColumns()); System.out.println("使用visitor排序:" + statVisitor.getOrderByColumns());
结果如下:
使用visitor数据表:{user=Select, order_info=Select, age_table=Select} 使用visitor字段:[user.id, order_info.user_id, user.name, user.age, age_table.age, age_table.user_id, user.score] 查询的字段:`user`.id,id 查询的字段:`user`.`name`,`name` 查询的字段:`user`.age,age 查询的字段:age_table.age,age 使用visitor条件:[user.id = 1, order_info.user_id =, user.name = xxx, user.age = null, age_table.user_id = 1] 使用visitor分组:[user.id, user.score] 使用visitor排序:[user.id, user.score]
可以看到涉及的表,字段,查询条件全都有了,且还可以区分当前字段是查询的,还是where条件的,还是join条件的。
如果想添加一个where,或者移除一个where怎么处理呢?
sqlSelectQueryBlock.addCondition("a.score = 100"); sqlSelectQueryBlock.removeCondition("a.id = 1"); System.out.println("更改条件后sql:" + sqlSelectQueryBlock.toString());
结果为:
SELECT a.id, a.`name`, a.age FROM `user` a LEFT JOIN order_info o ON a.id = o.user_id WHERE a.name = 'xxx' AND a.age = ( SELECT age FROM age_table age WHERE age.user_id = 1 ) AND a.score = 100 GROUP BY a.id, a.score HAVING MAX(a.id) > 1 ORDER BY a.id DESC, a.score ASC LIMIT 3, 10
可以看到,a.id = 1的条件已经没了,而且加了一个,a.score = 100 的条件。
到此基本的入门sqlParse已经讲完了,后面会再学习下源码,看下到底是怎么实现的。
版权声明:本文由星尘阁原创出品,转载请注明出处!