1. 概述
SQL 解析引擎,数据库中间件必备的功能和流程。Sharding-JDBC 在 1.5.0.M1 正式发布时,将 SQL 解析引擎从 Druid 替换成了自研的。新引擎仅解析分片上下文,对于 SQL 采用"半理解"理念,进一步提升性能和兼容性,同时降低了代码复杂度。
SQL 解析引擎有两大组件:
- Lexer:词法解析器。
- Parser:SQL解析器。
两者都是解析器,区别在于 Lexer 只做词法的解析,不关注上下文,将字符串拆解成 N 个词法。而 Parser 在 Lexer 的基础上,还需要理解 SQL 。打个比方:
SQL :SELECT * FROM t_user
Lexer
:[SELECT] [ * ] [FROM] [t_user]
Parser
:这是一条 [SELECT] 查询表为 [t_user] ,并且返回 [ * ] 所有字段的 SQL。
2. Lexer 词法解析器
Lexer 原理:顺序解析 SQL,将字符串拆解成 N 个词法。
核心代码如下:
/**
* 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
(词法标记)。我们来执行一次,看看 SQL 会被拆解成哪些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 |
眼尖的同学可能看到了 Tokenizer。对的,它是 Lexer 的好基佬,负责分词。
我们来总结下,Lexer#nextToken()
方法里,使用 skipIgnoredToken()
方法跳过忽略的 Token(如空格、注释),通过 isXXXX()
方法判断好下一个 Token 的类型后,交给 Tokenizer 进行分词返回 Token。
由于不同数据库遵守 SQL 规范略有不同,所以不同的数据库对应不同的 Lexer。
子 Lexer 通过重写方法实现自己独有的 SQL 语法。
3. Token 词法标记
Token
中一共有三个属性:
- TokenType type :词法标记类型
- String literals :词法字面量标记
- int endPosition : literals 在 SQL 里的结束位置
TokenType
词法标记类型,一共分成 4 个大类:
- Keyword :词法关键词
- Literals :词法字面量标记
- Symbol :词法符号标记
- Assist :词法辅助标记
3.1 Keyword 词法关键词
不同数据库有自己独有的词法关键词,例如 MySQL 熟知的分页 Limit。
我们以 MySQL 举个例子,当创建 MySQLLexer 时,会加载 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 {
/**
* 词法关键词Map
*/
private final Map<String, Keyword> tokens = new HashMap<>(1024);
public Dictionary(final Keyword... dialectKeywords) {
fill(dialectKeywords);
}
/**
* 装上默认词法关键词 + 方言词法关键词
* 不同的数据库有相同的默认词法关键词,有不同的方言关键词
*
* @param dialectKeywords 方言词法关键词
*/
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 词法字面量标记
Literals 词法字面量标记,一共分成 6 种:
IDENTIFIER :词法关键词
例如:表名,查询字段 等等。VARIABLE :变量
例如: SELECT @@VERSION 。在 MySQL 里,@代表用户变量,@@代表系统变量。CHARS :字符串
例如: SELECT "123" 。HEX :十六进制
以“0x”开头的数据。INT :整数
例如: SELECT * FROM t_user WHERE id = 1。FLOAT :浮点数
例如: SELECT * FROM t_user WHERE id = 1.0。
3.3 Symbol 词法符号标记
词法符号标记。例如:"{", "}", ">=" 等等。
解析核心代码如下:
// Lexer.java
/**
* 是否是 符号
*
* @see Tokenizer#scanSymbol()
* @return 是否
*/
private boolean isSymbolBegin() {
return CharType.isSymbol(getCurrentChar(0));
}
// CharType.java
/**
* 判断是否为符号.
*
* @param ch 待判断的字符
* @return 是否为符号
*/
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
/**
* 扫描符号.
*
* @return 符号标记
*/
public Token scanSymbol() {
int length = 0;
while (CharType.isSymbol(charAt(offset + length))) {
length++;
}
String literals = input.substring(offset, offset + length);
// 倒序遍历,查询符合条件的 符号。例如 literals = ";;",会是拆分成两个 ";"。如果基于正序,literals = "<=",会被解析成 "<" + "="。
Symbol symbol;
while (null == (symbol = Symbol.literalsOf(literals))) {
literals = input.substring(offset, offset + --length);
}
return new Token(symbol, literals, offset + length);
}
3.4 Assist 词法辅助标记
Assist 词法辅助标记,一共分成 2 种:
- END :分析结束
- ERROR :分析错误。
4. 结束
Lexer 词法解析已经讲解完毕,下一节我们将讨论 SQL 解析,尽请关注!