网站首页> 文章专栏> 深入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已经讲完了,后面会再学习下源码,看下到底是怎么实现的。
版权声明:本文由星尘阁原创出品,转载请注明出处!