1. 概述
SQL 解析引擎,數(shù)據(jù)庫中間件必備的功能和流程筝野。Sharding-JDBC 在 1.5.0.M1 正式發(fā)布時(shí)晌姚,將 SQL 解析引擎從 Druid 替換成了自研的粤剧。新引擎僅解析分片上下文歇竟,對(duì)于 SQL 采用"半理解"理念,進(jìn)一步提升性能和兼容性抵恋,同時(shí)降低了代碼復(fù)雜度焕议。
SQL 解析引擎有兩大組件:
- Lexer:詞法解析器。
- Parser:SQL解析器弧关。
兩者都是解析器盅安,區(qū)別在于 Lexer 只做詞法的解析唤锉,不關(guān)注上下文,將字符串拆解成 N 個(gè)詞法别瞭。而 Parser 在 Lexer 的基礎(chǔ)上窿祥,還需要理解 SQL 。打個(gè)比方:
SQL :SELECT * FROM t_user
Lexer
:[SELECT] [ * ] [FROM] [t_user]
Parser
:這是一條 [SELECT] 查詢表為 [t_user] 蝙寨,并且返回 [ * ] 所有字段的 SQL晒衩。
2. Lexer 詞法解析器
Lexer 原理:順序解析 SQL,將字符串拆解成 N 個(gè)詞法墙歪。
核心代碼如下:
/**
* Lexical analysis.
*
* @author zhangliang
*/
@RequiredArgsConstructor
public class Lexer {
@Getter
private final String input;
private final Dictionary dictionary;
private int offset;
@Getter
private Token currentToken;
/**
* Analyse next token.
*/
public final void nextToken() {
skipIgnoredToken();
if (isVariableBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanVariable();
} else if (isNCharBegin()) {
currentToken = new Tokenizer(input, dictionary, ++offset).scanChars();
} else if (isIdentifierBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanIdentifier();
} else if (isHexDecimalBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanHexDecimal();
} else if (isNumberBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanNumber();
} else if (isSymbolBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanSymbol();
} else if (isCharsBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanChars();
} else if (isEnd()) {
currentToken = new Token(Assist.END, "", offset);
} else {
throw new SQLParsingException(this, Assist.ERROR);
}
offset = currentToken.getEndPosition();
}
private void skipIgnoredToken() {
offset = new Tokenizer(input, dictionary, offset).skipWhitespace();
while (isHintBegin()) {
offset = new Tokenizer(input, dictionary, offset).skipHint();
offset = new Tokenizer(input, dictionary, offset).skipWhitespace();
}
while (isCommentBegin()) {
offset = new Tokenizer(input, dictionary, offset).skipComment();
offset = new Tokenizer(input, dictionary, offset).skipWhitespace();
}
}
}
通過nextToken()
方法听系,不斷解析出Token
(詞法標(biāo)記)。我們來執(zhí)行一次虹菲,看看 SQL 會(huì)被拆解成哪些Token
靠胜。
SQL :SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.user_id=? AND o.order_id=?
literals | TokenType類 | TokenType值 | endPosition |
---|---|---|---|
SELECT | DefaultKeyword | SELECT | 6 |
i | Literals | IDENTIFIER | 8 |
. | Symbol | DOT | 9 |
* | Symbol | STAR | 10 |
FROM | DefaultKeyword | FROM | 15 |
t_order | Literals | IDENTIFIER | 23 |
o | Literals | IDENTIFIER | 25 |
JOIN | DefaultKeyword | JOIN | 30 |
t_order_item | Literals | IDENTIFIER | 43 |
i | Literals | IDENTIFIER | 45 |
ON | DefaultKeyword | ON | 48 |
o | Literals | IDENTIFIER | 50 |
. | Symbol | DOT | 51 |
order_id | Literals | IDENTIFIER | 59 |
= | Symbol | EQ | 60 |
i | Literals | IDENTIFIER | 61 |
. | Symbol | DOT | 62 |
order_id | Literals | IDENTIFIER | 70 |
WHERE | DefaultKeyword | WHERE | 76 |
o | Literals | IDENTIFIER | 78 |
. | Symbol | DOT | 79 |
user_id | Literals | IDENTIFIER | 86 |
= | Symbol | EQ | 87 |
? | Symbol | QUESTION | 88 |
AND | DefaultKeyword | AND | 92 |
o | Literals | IDENTIFIER | 94 |
. | Symbol | DOT | 95 |
order_id | Literals | IDENTIFIER | 103 |
= | Symbol | EQ | 104 |
? | Symbol | QUESTION | 105 |
Assist | END | 105 |
眼尖的同學(xué)可能看到了 Tokenizer。對(duì)的毕源,它是 Lexer 的好基佬浪漠,負(fù)責(zé)分詞。
我們來總結(jié)下霎褐,Lexer#nextToken()
方法里郑藏,使用 skipIgnoredToken()
方法跳過忽略的 Token(如空格、注釋)瘩欺,通過 isXXXX()
方法判斷好下一個(gè) Token 的類型后必盖,交給 Tokenizer 進(jìn)行分詞返回 Token。
由于不同數(shù)據(jù)庫遵守 SQL 規(guī)范略有不同俱饿,所以不同的數(shù)據(jù)庫對(duì)應(yīng)不同的 Lexer歌粥。
子 Lexer 通過重寫方法實(shí)現(xiàn)自己獨(dú)有的 SQL 語法。
3. Token 詞法標(biāo)記
Token
中一共有三個(gè)屬性:
- TokenType type :詞法標(biāo)記類型
- String literals :詞法字面量標(biāo)記
- int endPosition : literals 在 SQL 里的結(jié)束位置
TokenType
詞法標(biāo)記類型拍埠,一共分成 4 個(gè)大類:
- Keyword :詞法關(guān)鍵詞
- Literals :詞法字面量標(biāo)記
- Symbol :詞法符號(hào)標(biāo)記
- Assist :詞法輔助標(biāo)記
3.1 Keyword 詞法關(guān)鍵詞
不同數(shù)據(jù)庫有自己獨(dú)有的詞法關(guān)鍵詞失驶,例如 MySQL 熟知的分頁 Limit。
我們以 MySQL 舉個(gè)例子枣购,當(dāng)創(chuàng)建 MySQLLexer 時(shí)嬉探,會(huì)加載 DefaultKeyword 和 MySQLKeyword。核心代碼如下:
// MySQLLexer.java
public final class MySQLLexer extends Lexer {
/**
* 字典
*/
private static Dictionary dictionary = new Dictionary(MySQLKeyword.values());
public MySQLLexer(final String input) {
super(input, dictionary);
}
}
// Dictionary.java
public final class Dictionary {
/**
* 詞法關(guān)鍵詞Map
*/
private final Map<String, Keyword> tokens = new HashMap<>(1024);
public Dictionary(final Keyword... dialectKeywords) {
fill(dialectKeywords);
}
/**
* 裝上默認(rèn)詞法關(guān)鍵詞 + 方言詞法關(guān)鍵詞
* 不同的數(shù)據(jù)庫有相同的默認(rèn)詞法關(guān)鍵詞棉圈,有不同的方言關(guān)鍵詞
*
* @param dialectKeywords 方言詞法關(guān)鍵詞
*/
private void fill(final Keyword... dialectKeywords) {
for (DefaultKeyword each : DefaultKeyword.values()) {
tokens.put(each.name(), each);
}
for (Keyword each : dialectKeywords) {
tokens.put(each.toString(), each);
}
}
}
3.2 Literals 詞法字面量標(biāo)記
Literals 詞法字面量標(biāo)記涩堤,一共分成 6 種:
IDENTIFIER :詞法關(guān)鍵詞
例如:表名,查詢字段 等等分瘾。VARIABLE :變量
例如: SELECT @@VERSION 胎围。在 MySQL 里,@代表用戶變量,@@代表系統(tǒng)變量白魂。CHARS :字符串
例如: SELECT "123" 汽纤。HEX :十六進(jìn)制
以“0x”開頭的數(shù)據(jù)。INT :整數(shù)
例如: SELECT * FROM t_user WHERE id = 1福荸。FLOAT :浮點(diǎn)數(shù)
例如: SELECT * FROM t_user WHERE id = 1.0蕴坪。
3.3 Symbol 詞法符號(hào)標(biāo)記
詞法符號(hào)標(biāo)記。例如:"{", "}", ">=" 等等敬锐。
解析核心代碼如下:
// Lexer.java
/**
* 是否是 符號(hào)
*
* @see Tokenizer#scanSymbol()
* @return 是否
*/
private boolean isSymbolBegin() {
return CharType.isSymbol(getCurrentChar(0));
}
// CharType.java
/**
* 判斷是否為符號(hào).
*
* @param ch 待判斷的字符
* @return 是否為符號(hào)
*/
public static boolean isSymbol(final char ch) {
return '(' == ch || ')' == ch || '[' == ch || ']' == ch || '{' == ch || '}' == ch || '+' == ch || '-' == ch || '*' == ch || '/' == ch || '%' == ch || '^' == ch || '=' == ch
|| '>' == ch || '<' == ch || '~' == ch || '!' == ch || '?' == ch || '&' == ch || '|' == ch || '.' == ch || ':' == ch || '#' == ch || ',' == ch || ';' == ch;
}
// Tokenizer.java
/**
* 掃描符號(hào).
*
* @return 符號(hào)標(biāo)記
*/
public Token scanSymbol() {
int length = 0;
while (CharType.isSymbol(charAt(offset + length))) {
length++;
}
String literals = input.substring(offset, offset + length);
// 倒序遍歷辞嗡,查詢符合條件的 符號(hào)。例如 literals = ";;"滞造,會(huì)是拆分成兩個(gè) ";"续室。如果基于正序,literals = "<="谒养,會(huì)被解析成 "<" + "="挺狰。
Symbol symbol;
while (null == (symbol = Symbol.literalsOf(literals))) {
literals = input.substring(offset, offset + --length);
}
return new Token(symbol, literals, offset + length);
}
3.4 Assist 詞法輔助標(biāo)記
Assist 詞法輔助標(biāo)記,一共分成 2 種:
- END :分析結(jié)束
- ERROR :分析錯(cuò)誤买窟。
4. 結(jié)束
Lexer 詞法解析已經(jīng)講解完畢丰泊,下一節(jié)我們將討論 SQL 解析,盡請(qǐng)關(guān)注始绍!