likes
comments
collection
share

Druid SQL 解析原理介绍及使用

作者站长头像
站长
· 阅读数 16

本篇文章主要以使用为主,详细介绍如何对一条SQL语句进行解析以及SQL改写等高级操作,SQL解析在分布式数据领域使用广泛,如分库分表组件路由模块,对业务SQL进行解析,提取条件路由。典型代表有Mycat等。如何工作中涉及到SQL解析或者改写等需求,本篇文章基本的需求应该都可以解决。

SQL解析器涉及到的内容后主要由三部分构成,如下:

1、Parser

    a、词法分析

    b、语法分析

2、AST(Abstract Syntax Tree,抽象语法树)

3、Visitor:遍历SQL元素

AST介绍

在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示,Druid 解析SQL也一样,会遵循一定的规则将SQL分析并构建成语法树AST。

Parser主要的作用是生成AST,Parser主要有两部分组成:词法分析、语法分析

Druid收到一条SQL后,比如select a, b , from userTable where user_id =10 需要解析出没一个单词,并记录单词位置等,词法解析阶段,并不需要理解这条SQL的含义,专业术语 Lexer ​

Druid SQL 解析原理介绍及使用

Druid SQL 解析原理介绍及使用

Druid SQL 解析原理介绍及使用

Druid SQL 解析原理介绍及使用

词法分析完成后就是语法分析,作用就是要明确SQL的含义,比如"name"这个单词,我们可能知道这个单词知道是由字母n、a、m、e组成,如果不知道含义,那就没实际意义,但是要知道是什么意思,就是语法分析要做的事了,语法分析的结果就是要明确这个单词的含义。

AST 仅仅是语义的表示,但如何对这个语义进行表达,便需要去访问这棵 AST,看它到底表达什么含义。通常遍历语法树,使用 VISITOR 模式去遍历,从根节点开始遍历,一直到最后一个叶子节点,在遍历的过程中,便不断地收集信息到一个上下文中,整个遍历过程完成后,对这棵树所表达的语法含义,已经被保存到上下文了。有时候一次遍历还不够,需要二次遍历。遍历的方式,广度优先的遍历方式是最常见的,或者是不用visitor,我们自己知道AST结构后自己手动遍历AST结构也是没有问题的,就是很繁琐,我觉得手动遍历和使用的visitor的区别就类似你用JQuery和JavaScript实现同一个功能的区别一个,有时实现一个功能使用Visitor可能就是十几行代码,自己遍历Statement可能要上百行代码,个人理解。

通过Parser生成完整的AST抽象语法树。

2、Druid SQL中AST节点类型

常用的AST节点主要有三种类型:SQLObject、SQLExpr、SQLStatement,其中最常使用的就莫过于SQLStatement ,其子类常见的有DruidSelectStatement、DruidInsertStatement、DruidUpdateStatement等

public interface SQLStatement extends SQLObject {}
public interface SQLObject {}
public interface SQLExpr extends SQLObject, Cloneable {}

2.1、常见的SQLExpr

package com.alibaba.druid.sql.ast.expr;

// 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;
}

2.2、常见的SQLStatement

最常用的Statement当然是SELECT/UPDATE/DELETE/INSERT,他们分别是

package com.alibaba.druid.sql.ast.statement;

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;
}

2.3、常见的SQLTableSource

常见的SQLTableSource包括SQLExprTableSource、SQLJoinTableSource、SQLSubqueryTableSource、SQLWithSubqueryClause.Entry

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;
}

/* 
例如
WITH RECURSIVE ancestors AS (
    SELECT *
    FROM org
    UNION
    SELECT f.*
    FROM org f, ancestors a
    WHERE f.id = a.parent_id
)
SELECT *
FROM ancestors;

这里的ancestors AS (...) 是一个SQLWithSubqueryClause.Entry
*/
class SQLWithSubqueryClause {
    static class Entry extends SQLTableSourceImpl { 
         SQLSelect subQuery;
    }
}

2.4、SQLSelect & SQLSelectQuery

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;
    SQLExprTableSource into;
    SQLExpr where;
    SQLSelectGroupByClause groupBy;
    SQLOrderBy orderBy;
    SQLLimit limit;
}

class SQLUnionQuery implements SQLSelectQuery {
    SQLSelectQuery left;
    SQLSelectQuery right;
    SQLUnionOperator operator; // UNION/UNION_ALL/MINUS/INTERSECT
}

2.5、SQLCreateTableStatement

建表语句包含了一系列方法,用于方便各种操作

public class SQLCreateTableStatement extends SQLStatementImpl implements SQLDDLStatement, SQLCreateStatement {
    SQLExprTableSource tableSource;
    List<SQLTableElement> tableElementList;
    Select select;

    // 忽略大小写的查找SQLCreateTableStatement中的SQLColumnDefinition
    public SQLColumnDefinition findColumn(String columName) {}

    // 忽略大小写的查找SQLCreateTableStatement中的column关联的索引
    public SQLTableElement findIndex(String columnName) {}

    // 是否外键依赖另外一个表
    public boolean isReferenced(String tableName) {}
}

3、生成抽象语法树(AST)

通过传入的文本即sql生成AST,使用了Mysql的Parser

  public SQLStatement parserSQL(String originSql) throws SQLSyntaxErrorException {
        SQLStatementParser parser = new MySqlStatementParser(originSql);

        /**
         * thrown SQL SyntaxError if parser error
         */
        try {
            List<SQLStatement> list = parser.parseStatementList();
            if (list.size() > 1) {
                throw new SQLSyntaxErrorException("MultiQueries is not supported,use single query instead ");
            }
            return list.get(0);
        } catch (Exception t) {
            LOGGER.info("routeNormalSqlWithAST", t);
            if (t.getMessage() != null) {
                throw new SQLSyntaxErrorException(t.getMessage());
            } else {
                throw new SQLSyntaxErrorException(t);
            }
        }
    }

或者根据数据库类型生成,Druid根据不同的数据库类型生成AST

/**
 * @author Qi.qingshan
 */
public class SqlParser {
    public static void main(String[] args) throws SQLSyntaxErrorException {
        String sql = "";
        String dbType = "oracle";
        SQLStatement statement = parser(sql, dbType);

    }
    public static SQLStatement parser(String sql,String dbType) throws SQLSyntaxErrorException {
        List<SQLStatement> list = SQLUtils.parseStatements(sql, dbType);
        if (list.size() > 1) {
            throw new SQLSyntaxErrorException("MultiQueries is not supported,use single query instead ");
        }
        return list.get(0);
    }
}

语法树AST生成后,我们可以根据语法树的组成提取我们需要的东西,比如我们要提取 select name ,id from acct where id =10 这条语句的表名,并将语句中的表名替换为acct_1

/**
 * @author Qi.qingshan
 */
public class SqlParser {
    public static void main(String[] args) throws SQLSyntaxErrorException {
        String sql = "select  name ,id from acct where id =10";
        String dbType = "mysql";
        System.out.println("原始SQL 为 : "+sql);
        SQLSelectStatement statement = (SQLSelectStatement) parser(sql, dbType);
        SQLSelect select = statement.getSelect();
        SQLSelectQueryBlock query = (SQLSelectQueryBlock) select.getQuery();
        SQLExprTableSource tableSource = (SQLExprTableSource) query.getFrom();
        String tableName = tableSource.getExpr().toString();
        System.out.println("获取的表名为  tableName :" + tableName);
        //修改表名为acct_1
        tableSource.setExpr("acct_1");
        System.out.println("修改表名后的SQL 为 : [" + statement.toString() +"]");
    }
    public static SQLStatement parser(String sql,String dbType) throws SQLSyntaxErrorException {
        List<SQLStatement> list = SQLUtils.parseStatements(sql, dbType);
        if (list.size() > 1) {
            throw new SQLSyntaxErrorException("MultiQueries is not supported,use single query instead ");
        }
        return list.get(0);
    }
}

4、Visitor

Druid提供了多种默认实现的Visitor,可以满足基本需求,如果默认提供的不满足需求,可自行实现自定义Visitor。

比如我们要统计下一条SQL中涉及了哪些表 select name ,id ,select money from user from acct where id =10,如果我们不用visitor,自行遍历AST,能实现,但是很繁琐,下边内部结构图

/**
 * @author Qi.qingshan
 * @date 20190526
 */
public class SqlParser {
    public static void main(String[] args) throws SQLSyntaxErrorException {
        String sql = "select money from acct where id =10";
        String dbType = "mysql";
        System.out.println("原始SQL 为 : "+sql);
        SQLSelectStatement statement = (SQLSelectStatement) parser(sql, dbType);
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        statement.accept(visitor);
        System.out.println(visitor.getTables().toString());
    }
    public static SQLStatement parser(String sql,String dbType) throws SQLSyntaxErrorException {
        List<SQLStatement> list = SQLUtils.parseStatements(sql, dbType);
        if (list.size() > 1) {
            throw new SQLSyntaxErrorException("MultiQueries is not supported,use single query instead ");
        }
        return list.get(0);
    }
}