网站首页> 文章专栏> 深入druid之sqlparse,sql语法是如何被解析的
深入druid之sqlparse,sql语法是如何被解析的

一. druid是什么

druid是alibaba在github上开源的项目,它是一个组件库,包含了数据库连接池、Sql parser等组件。

DruidDataSource号称是Java语言中最全面最好的连接池,理论上支持所有的JDBC驱动的数据源(实际测试并未把所有数据源测试一遍)。我们对他的了解也更多地是在如何将它作为连接池应用在程序中,实际它还有其他的组成。

github地址:https://github.com/alibaba/druid

二. sqlParse是什么,能做什么

可能我们在学Mysql的时候会想过,sql语法是怎么解析呢,执行的时候是怎么知道sql语法是正确的呢?当我们编写一个巨长巨复杂的sql时,也会想这个执行是按照什么顺序的呢?其实我之前在面试京东的时候,面试官也问过mysql的解析器了解过吗,是怎么解析sql的,当时的我大概也就只知道是有词法分析,语法分析,具体怎么实现的实际并不知道。

druid中的SQL parser是druid的重要组成部分,SQL parser在druid中承担着重要责任,如:SQL解析、防御SQL注入、格式化、分库分表等。sqlParse实际就是一套对sql语法的解析,我们利用它就可以分析sql是否正确,从sql中获取表名,列名,where条件等,也可以在sql基础上添加一些语法。

三. sqlParse组成

Druid SQL Parser分三个模块:

- Parser

- AST

- Visitor

3.1 Parser

parser是将输入文本转换为ast(抽象语法树),parser有包括两个部分,Parser和Lexer,其中Lexer实现词法分析,Parser实现语法分析。

3.2 AST

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中错误提示、自动补全、编译器、语法翻译、重构、代码混淆压缩转换等。

3.3 visitor

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示例

以上我们大概了解了sqlParse是什么,基本原理,以及能做什么,现在我们来写个demo实践下。

druid是java实现的,我们先引入包

  1.         <dependency>
  2.             <groupId>com.alibaba</groupId>
  3.             <artifactId>druid</artifactId>
  4.             <version>1.1.12</version>
  5.         </dependency>

注意,druid的版本是有差异的,有的版本并不向下兼容,曾经升级过导致线上爆炸。。。

准备一条sql:

  1.         SELECT
  2.           a.id,
  3.           a.`name`,
  4.           a.age
  5.         FROM
  6.           `user` a
  7.           LEFT JOIN order_info o
  8.           ON a.id = o.user_id
  9.         WHERE a.id = 1
  10.           AND a.name = 'xxx'
  11.           AND a.age = (SELECT age FROM age_table age WHERE age.user_id = 1)
  12.         GROUP BY a.id,
  13.           a.score
  14.         HAVING MAX(a.id) > 1
  15.         ORDER BY a.id DESC,
  16.           a.score ASC
  17.         LIMIT 3, 10;

不算是复杂的sql,包含join,where,子查询,group by,order by , limit等基本语法,我们看下sqlParse是如何解析的。

  1.         String dbType = JdbcConstants.MYSQL;
  2.         List statementList = SQLUtils.parseStatements(sql, dbType);
  3.         SQLStatement statement = statementList.get(0);
  4.         // 只考虑查询语句
  5.         SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) statement;
  6.         SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery();
  7.         // 非union的查询语句
  8.         if (sqlSelectQuery instanceof SQLSelectQueryBlock) {
  9.             SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery;
  10.             // 获取字段列表
  11.             List selectItems = sqlSelectQueryBlock.getSelectList();
  12.             System.out.println("字段信息:=========");
  13.             selectItems.forEach(System.out::println);
  14.             // 获取表
  15.             SQLTableSource table = sqlSelectQueryBlock.getFrom();
  16.             System.out.println("表信息:=========");
  17.             if (table instanceof SQLExprTableSource) {
  18.                 // 普通单表
  19.                 SQLExprTableSource sqlExprTable = (SQLExprTableSource) table;
  20.                 System.out.println("当前为单表,表名为:" + sqlExprTable.getName());
  21.                 System.out.println("别名为:" + sqlExprTable.getAlias());
  22.             } else if (table instanceof SQLJoinTableSource) {
  23.                 SQLJoinTableSource joinTable = (SQLJoinTableSource) table;
  24.                 // join多表
  25.                 System.out.println("当前为join表,join类型为:" + joinTable.getJoinType());
  26.                 System.out.println("join条件为:" + joinTable.getCondition());
  27.             } else if (table instanceof SQLSubqueryTableSource) {
  28.                 // 子查询作为表
  29.                 SQLSubqueryTableSource subQueryTable = (SQLSubqueryTableSource) table;
  30.                 System.out.println("参数:" + subQueryTable.getAttributes());
  31.             }
  32.             // 获取where条件
  33.             SQLExpr where = sqlSelectQueryBlock.getWhere();
  34.             // 如果是二元表达式
  35.             if (where instanceof SQLBinaryOpExpr) {
  36.                 SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) where;
  37.                 SQLExpr left = sqlBinaryOpExpr.getLeft();
  38.                 SQLBinaryOperator operator = sqlBinaryOpExpr.getOperator();
  39.                 SQLExpr right = sqlBinaryOpExpr.getRight();
  40.                 System.out.println("where信息:=========");
  41.                 System.out.println("db类型:" + sqlBinaryOpExpr.getDbType());
  42.                 System.out.println("where字段:" + left.toString());
  43.                 System.out.println("where类型:" + operator.getName());
  44.                 System.out.println("where值:" + right.toString());
  45.             } else if (where instanceof SQLInSubQueryExpr) {
  46.                 // 如果是子查询
  47.                 SQLInSubQueryExpr sqlInSubQueryExpr = (SQLInSubQueryExpr) where;
  48.                 // 处理---------------------
  49.             }
  50.             // 获取分组
  51.             SQLSelectGroupByClause groupBy = sqlSelectQueryBlock.getGroupBy();
  52.             System.out.println("分组信息:=========");
  53.             groupBy.getItems().forEach(System.out::println);
  54.             System.out.println("分组条件:" + groupBy.getHaving().toString());
  55.             // 获取排序
  56.             SQLOrderBy orderBy = sqlSelectQueryBlock.getOrderBy();
  57.             System.out.println("排序信息:=========");
  58.             orderBy.getItems().forEach(-> {
  59.                 SQLOrderingSpecification type = o.getType();
  60.                 SQLExpr expr = o.getExpr();
  61.                 System.out.println("排序字段:" + expr);
  62.                 System.out.println("排序类型:" + type.name());
  63.             });
  64.             // 获取分页
  65.             SQLLimit limit = sqlSelectQueryBlock.getLimit();
  66.             System.out.println("分页信息:=========");
  67.             SQLExpr rowCount = limit.getRowCount();
  68.             SQLExpr offset = limit.getOffset();
  69.             System.out.println("偏移量:" + offset.toString());
  70.             System.out.println("获取数量:" + rowCount.toString());
  71.         } else if (sqlSelectQuery instanceof SQLUnionQuery) {
  72.             // union的查询语句
  73.             // 处理---------------------
  74.         }

可以看出通过这个直接就返回了一个 SQLStatement对象集合,,每一个SQLStatement代表一条完整的SQL语句,如果我们sql是多条用;分割的话,那么就会是个list,如果只有一条sql,那么list就只有一个元素。

  1.         String dbType = JdbcConstants.MYSQL;
  2.         List statementList = SQLUtils.parseStatements(sql, dbType);

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

  1. class SQLSelectStatement implements SQLStatement {
  2.     SQLSelect select;
  3. }
  4. class SQLUpdateStatement implements SQLStatement {
  5.     SQLExprTableSource tableSource;
  6.      List items;
  7.      SQLExpr where;
  8. }
  9. class SQLDeleteStatement implements SQLStatement {
  10.     SQLTableSource tableSource; 
  11.     SQLExpr where;
  12. }
  13. class SQLInsertStatement implements SQLStatement {
  14.     SQLExprTableSource tableSource;
  15.     List columns;
  16.     SQLSelect query;
  17. }

我们这条查询时select,所以转为SQLSelectStatement,SQLSelectStatement包含一个SQLSelect,SQLSelect包含一个SQLSelectQuery,都是组成的关系。SQLSelectQuery有主要的两个派生类,分别是SQLSelectQueryBlock和SQLUnionQuery。表示非union的查询和union的查询。

SQLSelectQueryBlock中包含的主要节点:


sql对象来源说明
字段SQLSelectItemssqlSelectQueryBlock.getSelectList()字段的列表

SQLTableSourcesqlSelectQueryBlock.getFrom()sql中的表,分为几种类型,普通单表,join多表,子查询作为的表
where条件
SQLExprsqlSelectQueryBlock.getWhere()分为普通查询,与子查询,多个where条件通过ast,二叉树的形式向下延申
group by
SQLSelectGroupByClausesqlSelectQueryBlock.getGroupBy()包含分组列表,与分组条件信息
order bySQLOrderBysqlSelectQueryBlock.getOrderBy()包含排序列表,与升降序信息
limitSQLLimitsqlSelectQueryBlock.getLimit()包含偏移量,与页容量


这么一拆分的话,我们的sql便成为了如下的一棵树:(点击图片可放大)


星尘阁出品


通过以上语法树对象,我们便获取了基本的sql信息,看下输出:


  1. 字段信息:=========
  2. a.id
  3. a.`name`
  4. a.age
  5. 表信息:=========
  6. 当前为join表,join类型为:LEFT_OUTER_JOIN
  7. join条件为:a.id = o.user_id
  8. where信息:=========
  9. db类型:mysql
  10. where字段:a.id = 1
  11. AND a.name = 'xxx'
  12. where类型:AND
  13. where值:a.age = (
  14. SELECT age
  15. FROM age_table age
  16. WHERE age.user_id = 1
  17. )
  18. 分组信息:=========
  19. a.id
  20. a.score
  21. 分组条件:MAX(a.id) > 1
  22. 排序信息:=========
  23. 排序字段:a.id
  24. 排序类型:DESC
  25. 排序字段:a.score
  26. 排序类型:ASC
  27. 分页信息:=========
  28. 偏移量:3
  29. 获取数量:10

这个是我们自己的查询,如果想系统的获取sql中包含的table name和字段列表的话,可以使用提供的visitor来获取。

  1. SchemaStatVisitor statVisitor = SQLUtils.createSchemaStatVisitor(dbType);
  2. for (SQLStatement stmt : statementList) {
  3.     stmt.accept(statVisitor);
  4. }
  5. System.out.println("使用visitor数据表:" + statVisitor.getTables());
  6. System.out.println("使用visitor字段:" + statVisitor.getColumns());
  7. for (TableStat.Column column : statVisitor.getColumns()) {
  8.     if (column.isSelect()){
  9.         System.out.println("查询的字段:" + column.getFullName() + "," + column.getName());
  10.     }
  11. }
  12. System.out.println("使用visitor条件:" + statVisitor.getConditions());
  13. System.out.println("使用visitor分组:" + statVisitor.getGroupByColumns());
  14. System.out.println("使用visitor排序:" + statVisitor.getOrderByColumns());

结果如下:

  1. 使用visitor数据表:{user=Select, order_info=Select, age_table=Select}
  2. 使用visitor字段:[user.id, order_info.user_id, user.name, user.age, age_table.age, age_table.user_id, user.score]
  3. 查询的字段:`user`.id,id
  4. 查询的字段:`user`.`name`,`name`
  5. 查询的字段:`user`.age,age
  6. 查询的字段:age_table.age,age
  7. 使用visitor条件:[user.id = 1, order_info.user_id =, user.name = xxx, user.age = null, age_table.user_id = 1]
  8. 使用visitor分组:[user.id, user.score]
  9. 使用visitor排序:[user.id, user.score]

可以看到涉及的表,字段,查询条件全都有了,且还可以区分当前字段是查询的,还是where条件的,还是join条件的。

如果想添加一个where,或者移除一个where怎么处理呢?

  1. sqlSelectQueryBlock.addCondition("a.score = 100");
  2. sqlSelectQueryBlock.removeCondition("a.id = 1");
  3. System.out.println("更改条件后sql:" + sqlSelectQueryBlock.toString());

结果为:

  1. SELECT a.id, a.`name`, a.age
  2. FROM `user` a
  3. LEFT JOIN order_info o ON a.id = o.user_id
  4. WHERE a.name = 'xxx'
  5. AND a.age = (
  6. SELECT age
  7. FROM age_table age
  8. WHERE age.user_id = 1
  9. )
  10. AND a.score = 100
  11. GROUP BY a.id, a.score
  12. HAVING MAX(a.id) > 1
  13. ORDER BY a.id DESC, a.score ASC
  14. LIMIT 3, 10

可以看到,a.id = 1的条件已经没了,而且加了一个,a.score = 100 的条件。


到此基本的入门sqlParse已经讲完了,后面会再学习下源码,看下到底是怎么实现的。


星尘阁出品星尘阁出品星尘阁出品星尘阁出品星尘阁出品星尘阁出品星尘阁出品星尘阁出品星尘阁出品星尘阁出品



版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/88




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论