數據庫中間件 Sharding-JDBC 源碼分析 —— SQL 解析(二)之SQL解析【推薦閱讀】

🙂🙂🙂關注微信公衆號:【芋艿的後端小屋】有福利: java

  1. RocketMQ / MyCAT / Sharding-JDBC 全部源碼分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋源碼 GitHub 地址
  3. 您對於源碼的疑問每條留言將獲得認真回覆。甚至不知道如何讀源碼也能夠請教噢
  4. 新的源碼解析文章實時收到通知。每週更新一篇左右
  5. 認真的源碼交流微信羣。


1. 概述

上篇文章《詞法解析》分享了詞法解析器Lexer是如何解析 SQL 裏的詞法。本文分享SQL解析引擎是如何解析與理解 SQL的。由於本文創建在《詞法解析》之上,你須要閱讀它後在開始這段旅程。🙂若是對詞法解析不徹底理解,請給個人公衆號芋艿的後端小屋留言,我會逐條認真耐心回覆。git

區別於 Lexer,Parser 理解SQLgithub

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

Parser 有三個組件:sql

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

SQLParsingEngine 調用 StatementParser 解析 SQL。
StatementParser 調用 SQLParser 解析 SQL 表達式。
SQLParser 調用 Lexer 解析 SQL 詞法。數據庫

😜 是否是以爲 SQLParser 和 StatementParser 看起來很接近?下文爲你揭開這個答案。express

Sharding-JDBC 正在收集使用公司名單:傳送門
🙂 你的登記,會讓更多人蔘與和使用 Sharding-JDBC。傳送門
Sharding-JDBC 也會所以,可以覆蓋更多的業務場景。傳送門
登記吧,騷年!傳送門
後端

2. SQLParsingEngine

SQLParsingEngine,SQL 解析引擎。其 #parse() 方法做爲 SQL 解析入口,自己不帶複雜邏輯,經過調用 SQL 對應的 StatementParser 進行 SQL 解析。微信

核心代碼以下:app

// 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();
}複製代碼

這個類其它方法很重要,邏輯相對簡單,咱們就不佔用篇幅了。你們必定要看喲,後面調用很是很是多。AbstractParser.java 傳送門。👼也能夠關注個人公衆號芋艿的後端小屋發送關鍵字【sjdbc】獲取增長方法內註釋的項目地址

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();
   }
}複製代碼

解析了 SQLExpression 有什麼用呢?咱們會在《查詢SQL解析》《插入SQL解析》《更新SQL解析》《刪除SQL解析》。留個懸念😈,關注個人公衆號芋艿的後端小屋實時收到新文更新通知

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解析》

5. 彩蛋

老鐵,是否是有丟丟長?
若是有地方錯誤,煩請指出🙂。
若是有地方不是很理解,能夠加個人公衆號芋艿的後端小屋留言,我會逐條認真耐心回覆。
若是以爲還湊合,勞駕分享朋友圈或者基佬。

《查詢SQL解析》已經寫了一半,預計很快...

相關文章
相關標籤/搜索