1. 程式人生 > 其它 >Sharding-JDBC 原始碼分析 —— SQL 解析(二)之SQL解析

Sharding-JDBC 原始碼分析 —— SQL 解析(二)之SQL解析

  • 1. 概述
  • 2. SQLParsingEngine
  • 3. SQLParser SQL解析器
    • 3.2.1 #parseExpression() 和 SQLExpression
    • 3.2.2 #parseAlias()
    • 3.2.3 #parseSingleTable()
    • 3.2.4 #skipJoin()
    • 3.2.5 #parseWhere()
    • 3.1 AbstractParser
    • 3.2 SQLParser
  • 4. StatementParser SQL語句解析器
    • 4.1 StatementParser
    • 4.2 Statement

1. 概述

區別於 Lexer,Parser 理解SQL

  • 提煉分片上下文
  • 標記需要SQL改寫的部分

Parser 有三個元件:

  • SQLParsingEngine :SQL 解析引擎
  • SQLParser :SQL 解析器
  • StatementParser :SQL語句解析器

SQLParsingEngine 呼叫 StatementParser 解析 SQL。 StatementParser 呼叫 SQLParser 解析 SQL 表示式。 SQLParser 呼叫 Lexer 解析 SQL 詞法。

? 是不是覺得 SQLParser 和 StatementParser 看起來很接近?下文為你揭開這個答案。

2. SQLParsingEngine

SQLParsingEngine,SQL 解析引擎。其 #parse() 方法作為 SQL 解析入口,本身不帶複雜邏輯,通過呼叫 SQL 對應的 StatementParser 進行 SQL 解析。

核心程式碼如下:

// SQLParsingEngine.java
public SQLStatement parse() {
   // 獲取 SQL解析器
   SQLParser sqlParser = getSQLParser();
   //
   sqlParser.skipIfEqual(Symbol.SEMI); // 跳過 ";"
   if (sqlParser.equalAny(DefaultKeyword.WITH)) { // WITH Syntax
       skipWith(sqlParser);
   }
   // 獲取對應 SQL語句解析器 解析SQL
   if (sqlParser.equalAny(DefaultKeyword.SELECT)) {
       return SelectParserFactory.newInstance(sqlParser).parse();
   }
   if (sqlParser.equalAny(DefaultKeyword.INSERT)) {
       return InsertParserFactory.newInstance(shardingRule, sqlParser).parse();
   }
   if (sqlParser.equalAny(DefaultKeyword.UPDATE)) {
       return UpdateParserFactory.newInstance(sqlParser).parse();
   }
   if (sqlParser.equalAny(DefaultKeyword.DELETE)) {
       return DeleteParserFactory.newInstance(sqlParser).parse();
   }
   throw new SQLParsingUnsupportedException(sqlParser.getLexer().getCurrentToken().getType());
}

3. SQLParser SQL解析器

SQLParser,SQL 解析器。和詞法解析器 Lexer 一樣,不同資料庫有不同的實現。

類圖如下(包含所有屬性和方法)(放大圖片):

3.1 AbstractParser

AbstractParser,SQLParser 的抽象父類,對 Lexer 簡單封裝。例如:

  • #skipIfEqual():判斷當前詞法標記型別是否與其中一個傳入值相等
  • #equalAny():判斷當前詞法標記型別是否與其中一個傳入值相等

這裡有一點我們需要注意,SQLParser 並不是等 Lexer 解析完詞法( Token ),再根據詞法去理解 SQL。而是,在理解 SQL 的過程中,呼叫 Lexer 進行分詞。

// SQLParsingEngine.java#parse()片段
if (sqlParser.equalAny(DefaultKeyword.SELECT)) {
    return SelectParserFactory.newInstance(sqlParser).parse();
}

// AbstractParser.java
public final boolean equalAny(final TokenType... tokenTypes) {
   for (TokenType each : tokenTypes) {
       if (each == lexer.getCurrentToken().getType()) {
           return true;
       }
   }
   return false;
}
  • ↑↑↑ 判斷當前詞法是否為 SELECT。實際 AbstractParser 只知道當前詞法,並不知道後面還有哪些詞法,也不知道之前有哪些詞法。

我們來看 AbstractParser 裡比較複雜的方法 #skipParentheses() 幫助大家再理解下。請認真看程式碼註釋噢。

// AbstractParser.java
/**
* 跳過小括號內所有的詞法標記.
*
* @return 小括號內所有的詞法標記
*/
public final String skipParentheses() {
   StringBuilder result = new StringBuilder("");
   int count = 0;
   if (Symbol.LEFT_PAREN == getLexer().getCurrentToken().getType()) {
       final int beginPosition = getLexer().getCurrentToken().getEndPosition();
       result.append(Symbol.LEFT_PAREN.getLiterals());
       getLexer().nextToken();
       while (true) {
           if (equalAny(Symbol.QUESTION)) {
               increaseParametersIndex();
           }
           // 到達結尾 或者 匹配合適數的)右括號
           if (Assist.END == getLexer().getCurrentToken().getType() || (Symbol.RIGHT_PAREN == getLexer().getCurrentToken().getType() && 0 == count)) {
               break;
           }
           // 處理裡面有多個括號的情況,例如:SELECT COUNT(DISTINCT(order_id) FROM t_order
           if (Symbol.LEFT_PAREN == getLexer().getCurrentToken().getType()) {
               count++;
           } else if (Symbol.RIGHT_PAREN == getLexer().getCurrentToken().getType()) {
               count--;
           }
           // 下一個詞法
           getLexer().nextToken();
       }
       // 獲得括號內的內容
       result.append(getLexer().getInput().substring(beginPosition, getLexer().getCurrentToken().getEndPosition()));
       // 下一個詞法
       getLexer().nextToken();
   }
   return result.toString();
}

3.2 SQLParser

SQLParser,SQL 解析器,主要提供只考慮 SQL 塊的解析方法,不考慮 SQL 上下文。下文即將提到的 StatementParser 將 SQL 拆成對應的,呼叫 SQLParser 進行解析。? 這麼說,可能會有些抽象,我們下面來一起看。

SQLParser 看起來方法特別多,合併下一共 5 種:

方法

說明

#parseExpression()

解析表示式

#parseAlias()

解析別名

#parseSingleTable()

解析單表

#skipJoin()

跳過表關聯詞法

#parseWhere()

解析查詢條件

看了這 5 個方法是否有點理解了?SQLParser 不考慮 SQL 是 SELECT / INSERT / UPDATE / DELETE ,它考慮的是,給我的是 WHERE 處解析查詢條件,或是 INSERT INTO 解析單表 等,提供 SELECT / INSERT / UPDATE / DELETE 需要的 SQL 塊公用解析。

3.2.1 #parseExpression() 和 SQLExpression

SQLExpression,SQL表示式介面。目前 6 種實現:

說明

對應Token

SQLIdentifierExpression

標識表示式

Literals.IDENTIFIER

SQLPropertyExpression

屬性表示式

SQLNumberExpression

數字表達式

Literals.INT, Literals.HEX

SQLPlaceholderExpression

佔位符表示式

Symbol.QUESTION

SQLTextExpression

字元表示式

Literals.CHARS

SQLIgnoreExpression

分片中無需關注的SQL表示式

  • SQLPropertyExpression 例如: SELECT*FROM t_order o ORDER BY o.order_id 中的 o.order_idSQLPropertyExpression 從 SQLIdentifierExpression 進一步判斷解析而來。
  • SQLIgnoreExpression 例如: SELECT*FROM t_order o ORDER BY o.order_id%2 中的 o.order_id%2複合表示式都會解析成 SQLIgnoreExpression。

解析 SQLExpression 核心程式碼如下:

// SQLParser.java
/**
* 解析表示式.
*
* @return 表示式
*/
// TODO 完善Expression解析的各種場景
public final SQLExpression parseExpression() {
   // 解析表示式
   String literals = getLexer().getCurrentToken().getLiterals();
   final SQLExpression expression = getExpression(literals);
   // SQLIdentifierExpression 需要特殊處理。考慮自定義函式,表名.屬性情況。
   if (skipIfEqual(Literals.IDENTIFIER)) {
       if (skipIfEqual(Symbol.DOT)) { // 例如,ORDER BY o.uid 中的 "o.uid"
           String property = getLexer().getCurrentToken().getLiterals();
           getLexer().nextToken();
           return skipIfCompositeExpression() ? new SQLIgnoreExpression() : new SQLPropertyExpression(new SQLIdentifierExpression(literals), property);
       }
       if (equalAny(Symbol.LEFT_PAREN)) { // 例如,GROUP BY DATE(create_time) 中的 "DATE(create_time)"
           skipParentheses();
           skipRestCompositeExpression();
           return new SQLIgnoreExpression();
       }
       return skipIfCompositeExpression() ? new SQLIgnoreExpression() : expression;
   }
   getLexer().nextToken();
   return skipIfCompositeExpression() ? new SQLIgnoreExpression() : expression;
}
/**
* 獲得 詞法Token 對應的 SQLExpression
*
* @param literals 詞法字面量標記
* @return SQLExpression
*/
private SQLExpression getExpression(final String literals) {
   if (equalAny(Symbol.QUESTION)) {
       increaseParametersIndex();
       return new SQLPlaceholderExpression(getParametersIndex() - 1);
   }
   if (equalAny(Literals.CHARS)) {
       return new SQLTextExpression(literals);
   }
   // TODO 考慮long的情況
   if (equalAny(Literals.INT)) {
       return new SQLNumberExpression(Integer.parseInt(literals));
   }
   if (equalAny(Literals.FLOAT)) {
       return new SQLNumberExpression(Double.parseDouble(literals));
   }
   // TODO 考慮long的情況
   if (equalAny(Literals.HEX)) {
       return new SQLNumberExpression(Integer.parseInt(literals, 16));
   }
   if (equalAny(Literals.IDENTIFIER)) {
       return new SQLIdentifierExpression(SQLUtil.getExactlyValue(literals));
   }
   return new SQLIgnoreExpression();
}
/**
* 如果是 複合表示式,跳過。
*
* @return 是否跳過
*/
private boolean skipIfCompositeExpression() {
   if (equalAny(Symbol.PLUS, Symbol.SUB, Symbol.STAR, Symbol.SLASH, Symbol.PERCENT, Symbol.AMP, Symbol.BAR, Symbol.DOUBLE_AMP, Symbol.DOUBLE_BAR, Symbol.CARET, Symbol.DOT, Symbol.LEFT_PAREN)) {
       skipParentheses();
       skipRestCompositeExpression();
       return true;
   }
   return false;
}
/**
* 跳過剩餘複合表示式
*/
private void skipRestCompositeExpression() {
   while (skipIfEqual(Symbol.PLUS, Symbol.SUB, Symbol.STAR, Symbol.SLASH, Symbol.PERCENT, Symbol.AMP, Symbol.BAR, Symbol.DOUBLE_AMP, Symbol.DOUBLE_BAR, Symbol.CARET, Symbol.DOT)) {
       if (equalAny(Symbol.QUESTION)) {
           increaseParametersIndex();
       }
       getLexer().nextToken();
       skipParentheses();
   }
}

3.2.2 #parseAlias()

/**
* 解析別名.不僅僅是欄位的別名,也可以是表的別名。
*
* @return 別名
*/
public Optional<String> parseAlias() {
   // 解析帶 AS 情況
   if (skipIfEqual(DefaultKeyword.AS)) {
       if (equalAny(Symbol.values())) {
           return Optional.absent();
       }
       String result = SQLUtil.getExactlyValue(getLexer().getCurrentToken().getLiterals());
       getLexer().nextToken();
       return Optional.of(result);
   }
   // 解析別名
   // TODO 增加哪些資料庫識別哪些關鍵字作為別名的配置
   if (equalAny(Literals.IDENTIFIER, Literals.CHARS, DefaultKeyword.USER, DefaultKeyword.END, DefaultKeyword.CASE, DefaultKeyword.KEY, DefaultKeyword.INTERVAL, DefaultKeyword.CONSTRAINT)) {
       String result = SQLUtil.getExactlyValue(getLexer().getCurrentToken().getLiterals());
       getLexer().nextToken();
       return Optional.of(result);
   }
   return Optional.absent();
}

3.2.3 #parseSingleTable()

/**
* 解析單表.
*
* @param sqlStatement SQL語句物件
*/
public final void parseSingleTable(final SQLStatement sqlStatement) {
   boolean hasParentheses = false;
   if (skipIfEqual(Symbol.LEFT_PAREN)) {
       if (equalAny(DefaultKeyword.SELECT)) { // multiple-update 或者 multiple-delete
           throw new UnsupportedOperationException("Cannot support subquery");
       }
       hasParentheses = true;
   }
   Table table;
   final int beginPosition = getLexer().getCurrentToken().getEndPosition() - getLexer().getCurrentToken().getLiterals().length();
   String literals = getLexer().getCurrentToken().getLiterals();
   getLexer().nextToken();
   if (skipIfEqual(Symbol.DOT)) {
       getLexer().nextToken();
       if (hasParentheses) {
           accept(Symbol.RIGHT_PAREN);
       }
       table = new Table(SQLUtil.getExactlyValue(literals), parseAlias());
   } else {
       if (hasParentheses) {
           accept(Symbol.RIGHT_PAREN);
       }
       table = new Table(SQLUtil.getExactlyValue(literals), parseAlias());
   }
   if (skipJoin()) { // multiple-update 或者 multiple-delete
       throw new UnsupportedOperationException("Cannot support Multiple-Table.");
   }
   sqlStatement.getSqlTokens().add(new TableToken(beginPosition, literals));
   sqlStatement.getTables().add(table);
}

3.2.4 #skipJoin()

跳過表關聯詞法,支援 SELECT*FROM t_user,t_order WHERE..., SELECT*FROM t_user JOIN t_order ON...。下篇《查詢SQL解析》解析表會用到這個方法。

// SQLParser.java
/**
* 跳過表關聯詞法.
*
* @return 是否表關聯.
*/
public final boolean skipJoin() {
   if (skipIfEqual(DefaultKeyword.LEFT, DefaultKeyword.RIGHT, DefaultKeyword.FULL)) {
       skipIfEqual(DefaultKeyword.OUTER);
       accept(DefaultKeyword.JOIN);
       return true;
   } else if (skipIfEqual(DefaultKeyword.INNER)) {
       accept(DefaultKeyword.JOIN);
       return true;
   } else if (skipIfEqual(DefaultKeyword.JOIN, Symbol.COMMA, DefaultKeyword.STRAIGHT_JOIN)) {
       return true;
   } else if (skipIfEqual(DefaultKeyword.CROSS)) {
       if (skipIfEqual(DefaultKeyword.JOIN, DefaultKeyword.APPLY)) {
           return true;
       }
   } else if (skipIfEqual(DefaultKeyword.OUTER)) {
       if (skipIfEqual(DefaultKeyword.APPLY)) {
           return true;
       }
   }
   return false;
}

3.2.5 #parseWhere()

解析 WHERE 查詢條件。目前支援 AND 條件,不支援 OR 條件。近期 OR 條件支援的可能性比較低。另外條件這塊對括號解析需要繼續優化,實際使用請勿寫冗餘的括號。例如: SELECT*FROM tbl_name1 WHERE((val1=?)AND(val2=?))AND val3=?

根據不同的運算操作符,分成如下情況:

運算子

附加條件

方法

=

#parseEqualCondition()

IN

#parseInCondition()

BETWEEN

#parseBetweenCondition()

<, <=, >, >=

Oracle 或 SQLServer 分頁

#parseRowNumberCondition()

<, <=, >, >=

#parseOtherCondition()

LIKE

parseOtherCondition

程式碼如下:

// SQLParser.java
/**
* 解析所有查詢條件。
* 目前不支援 OR 條件。
*
* @param sqlStatement SQL
*/
private void parseConditions(final SQLStatement sqlStatement) {
   // AND 查詢
   do {
       parseComparisonCondition(sqlStatement);
   } while (skipIfEqual(DefaultKeyword.AND));
   // 目前不支援 OR 條件
   if (equalAny(DefaultKeyword.OR)) {
       throw new SQLParsingUnsupportedException(getLexer().getCurrentToken().getType());
   }
} 
// TODO 解析組合expr
/**
* 解析單個查詢條件
*
* @param sqlStatement SQL
*/
public final void parseComparisonCondition(final SQLStatement sqlStatement) {
   skipIfEqual(Symbol.LEFT_PAREN);
   SQLExpression left = parseExpression(sqlStatement);
   if (equalAny(Symbol.EQ)) {
       parseEqualCondition(sqlStatement, left);
       skipIfEqual(Symbol.RIGHT_PAREN);
       return;
   }
   if (equalAny(DefaultKeyword.IN)) {
       parseInCondition(sqlStatement, left);
       skipIfEqual(Symbol.RIGHT_PAREN);
       return;
   }
   if (equalAny(DefaultKeyword.BETWEEN)) {
       parseBetweenCondition(sqlStatement, left);
       skipIfEqual(Symbol.RIGHT_PAREN);
       return;
   }
   if (equalAny(Symbol.LT, Symbol.GT, Symbol.LT_EQ, Symbol.GT_EQ)) {
       if (left instanceof SQLIdentifierExpression && sqlStatement instanceof SelectStatement
               && isRowNumberCondition((SelectStatement) sqlStatement, ((SQLIdentifierExpression) left).getName())) {
           parseRowNumberCondition((SelectStatement) sqlStatement);
       } else if (left instanceof SQLPropertyExpression && sqlStatement instanceof SelectStatement
               && isRowNumberCondition((SelectStatement) sqlStatement, ((SQLPropertyExpression) left).getName())) {
           parseRowNumberCondition((SelectStatement) sqlStatement);
       } else {
           parseOtherCondition(sqlStatement);
       }
   } else if (equalAny(DefaultKeyword.LIKE)) {
       parseOtherCondition(sqlStatement);
   }
   skipIfEqual(Symbol.RIGHT_PAREN);
}

#parseComparisonCondition() 解析到 左SQL表示式(left) 和 運算子,呼叫相應方法進一步處理。我們選擇 #parseEqualCondition() 看下,其他方法有興趣跳轉 SQLParser 檢視。

// SQLParser.java
/**
* 解析 = 條件
*
* @param sqlStatement SQL
* @param left 左SQLExpression
*/
private void parseEqualCondition(final SQLStatement sqlStatement, final SQLExpression left) {
   getLexer().nextToken();
   SQLExpression right = parseExpression(sqlStatement);
   // 新增列
   // TODO 如果有多表,且找不到column是哪個表的,則不加入condition,以後需要解析binding table
   if ((sqlStatement.getTables().isSingleTable() || left instanceof SQLPropertyExpression)
           // 只有對路由結果有影響的才會新增到 conditions。SQLPropertyExpression 和 SQLIdentifierExpression 無法判斷,所以未加入 conditions
           && (right instanceof SQLNumberExpression || right instanceof SQLTextExpression || right instanceof SQLPlaceholderExpression)) {
       Optional<Column> column = find(sqlStatement.getTables(), left);
       if (column.isPresent()) {
           sqlStatement.getConditions().add(new Condition(column.get(), right), shardingRule);
       }
   }
}

#parseEqualCondition() 解析到 右SQL表示式(right),並判斷 左右SQL表示式 與路由邏輯是否有影響,如果有,則加入到 Condition。這個就是 #parseWhere() 的目的:解析 WHERE 查詢條件對路由有影響的條件。《路由》相關的邏輯,會單獨開文章介紹。這裡,我們先留有映像。

4. StatementParser SQL語句解析器

4.1 StatementParser

StatementParser,SQL語句解析器。每種 SQL,都有相應的 SQL語句解析器實現。不同資料庫,繼承這些 SQL語句解析器,實現各自 SQL 上的差異。大體結構如下:

SQLParsingEngine 根據不同 SQL 呼叫對應工廠建立 StatementParser。核心程式碼如下:

public final class SelectParserFactory {

    /**
     * 建立Select語句解析器.
     * 
     * @param sqlParser SQL解析器
     * @return Select語句解析器
     */
    public static AbstractSelectParser newInstance(final SQLParser sqlParser) {
        if (sqlParser instanceof MySQLParser) {
            return new MySQLSelectParser(sqlParser);
        }
        if (sqlParser instanceof OracleParser) {
            return new OracleSelectParser(sqlParser);
        }
        if (sqlParser instanceof SQLServerParser) {
            return new SQLServerSelectParser(sqlParser);
        }
        if (sqlParser instanceof PostgreSQLParser) {
            return new PostgreSQLSelectParser(sqlParser);
        }
        throw new UnsupportedOperationException(String.format("Cannot support sqlParser class [%s].", sqlParser.getClass()));
    } 
}

呼叫 StatementParser#parse() 實現方法,對 SQL 進行解析。具體解析過程,另開文章分享。

4.2 Statement

不同 SQL 解析後,返回對應的 SQL 結果,即 Statement。大體結構如下:

Statement 包含兩部分資訊:

  • 分片上下文:用於 SQL 路由。
  • SQL 標記物件:用於 SQL 改寫。

我們會在後文增刪改查SQL解析的過程中分享到它們。

4.3 預告

Parser

Statement

分享文章

SelectStatementParser

SelectStatement + AbstractSQLStatement

《查詢SQL解析》

InsertStatementParser

InsertStatement

《插入SQL解析》

UpdateStatementParser

UpdateStatement

《更新SQL解析》

DeleteStatementParser

DeleteStatement

《刪除SQL解析》