Druid SqlParser理解及使用入門

之前的項目中不多去思考SQL解析這個事情,即便在saas系統或者分庫分表的時候有涉及到也會有專門的處理方案,這些方案也對使用者隱藏了實現細節。html

而最近的這個數據項目裏面卻頻繁涉及到了對SQL的處理,原來只是簡單地瞭解Druid的SqlParser模塊就能夠解決,慢慢地問題變得愈來愈複雜,直到某天改動本身寫的SQL處理的代碼很痛苦的時候,意識到彷佛有必要更加地瞭解一下相關的內容才行。git

在瞭解學習的過程當中,發現學習使用SqlParser仍是得先了解ast(抽象語法樹)這個概念,一搜索相關內容要麼是編譯原理相關的知識,要麼是JavaScript的示例,光看Druid提供的SqlParser相關的Wiki文檔又似懂非懂,不知道從哪裏下手。github

無論怎麼樣,看了很多碎片化的相關內容之後也收穫了一些東西,這裏記錄下來。正則表達式

爲何要先了解ast?

ast全稱是abstract syntax tree,中文直譯抽象語法樹。sql

原先我以爲要使用SqlParser就照着wiki上的代碼步驟拷過來就行了唄,也確實如此,它快速解決了個人問題。但是正如上面所說,但願你的相關代碼寫得更好一點,或者更理解它是在幹嘛瞭解了ast會有很多的幫助。編程

SQL解析,本質上就是把SQL字符串給解析成ast,也就是說SqlParser的入參是SQL字符串,結果就是一個ast。你怎麼使用這個ast結果又是另一回事,你能夠修改ast,也能夠添加點東西等等,但整個過程都是圍繞着ast這個東西。數據結構

什麼是ast?

上面提了好幾回ast,那ast又是個什麼東西呢?maven

參照維基百科的說法,在計算機科學領域內,ast表示的是你寫的編程語言源代碼的抽象語法結構。如圖:編程語言

左邊是一個很是簡單的編程語言源代碼:1 + 2,作了一個加法計算,而當它被解析成ast之後如右邊的圖所示。咱們能夠看到ast存在三個節點,頂部的 + 表示一個加法節點,這個表達式組合了一、2兩個數值節點,由這三個組合在一塊兒的節點就組成了1+2這樣的語法結構。ide

咱們看到ast很清晰地用數據結構表示出了字符串源代碼,ast的每個節點均表示源代碼當中的一個語法結構。反過來思考一下,咱們能夠知道源代碼解析出來的ast是由不少這樣簡單的語法結構組合而成的,也就造成了一個複雜的語法樹。下面咱們看一個稍微複雜一點的,來自維基百科的示例

源代碼:

1 while b ≠ 0
2   if a > b
3     a = a − b
4   else
5     b = b − a
6 return a

語法樹:

這個語法樹也清晰地表示的源代碼程序,主要由一個while語法和if/else語法以及一些變量之類的組成。

到這裏,彷佛對源代碼和ast有了一個簡單的概念,可是仍是存在困惑,我爲何要把好好的代碼搞成這樣?它有什麼用?若是隻是修改語法,我用正則表達式修改字符串不是簡單嗎?

確實,有的時候直接處理字符串會是更快速更好的解決方式,可是當源程序語法很是複雜的時候字符串處理的複雜度已經不是一個簡單的事了。而ast則把這些字符串變成結構化的數據了,你能夠精確地知道一段代碼裏面有哪些變量名,函數名,參數等,你能夠很是精準地處理,相對於字符串處理來講,遍歷數據大大下降的處理難度。而ast也經常用在如IDE中錯誤提示、自動補全、編譯器、語法翻譯、重構、代碼混淆壓縮轉換等。

瞭解ast能夠參考文章:

https://mp.weixin.qq.com/s/UYzwVRPFas6hwe2U7R0eIg

http://www.javashuo.com/article/p-gqhtijbs-gc.html

http://www.javashuo.com/article/p-ehcxsygf-cx.html

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作了隱藏,咱們不須要去實現這樣一個字符串解析過程。

因而可知,咱們須要瞭解兩方面內容:

一、怎麼用SqlParser把SQL語句解析成ast;

二、SqlParser解析出來的ast是什麼樣的一個結構。

下面須要一點代碼來講明,因此先引入一下maven依賴

1 <dependency>
2      <groupId>com.alibaba</groupId>
3      <artifactId>druid</artifactId>
4      <version>1.1.12</version>
5 </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

相關文章
相關標籤/搜索