网站首页 文章专栏 深入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上完成。


image.png

怎么理解ast呢?

就拿基本的数学运算来说,1+1=2,就可以用下面的图表示


image.png


可以看到ast存在三个节点,顶部的 + 表示一个加法节点,这个表达式组合了1、2两个数值节点,由这三个组合在一起的节点就组成了1+1这样的语法结构,那么再复杂的逻辑怎么处理呢?比如4 / (1+1)* 2这个表达式。


image.png


通过多个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实现的,我们先引入包

        <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对象来源说明
字段SQLSelectItemssqlSelectQueryBlock.getSelectList()字段的列表

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


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


image.png


通过以上语法树对象,我们便获取了基本的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已经讲完了,后面会再学习下源码,看下到底是怎么实现的。




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

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




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