hibernate中antlr對於hql生成抽象語法樹原始碼解析
Hibernate版本5.1.11FInal
以一句update語句作為例子。
update com.tydhot.eninty.User set userName=:userName where userId=:userId
上面這句hql經過antlr的語法解析之後,得到的語法樹如下。
\-[50] Node: 'update' +-[22] Node: 'FROM' | \-[90] Node: 'RANGE' | \-[15] Node: '.' | +-[15] Node: '.' | | +-[15] Node: '.' | | | +-[108] Node: 'com' | | | \-[108] Node: 'tydhot' | | \-[108] Node: 'eninty' | \-[108] Node: 'User' +-[46] Node: 'set' | \-[105] Node: '=' | +-[108] Node: 'userName' | \-[127] Node: ':' | \-[108] Node: 'userName' \-[52] Node: 'where' \-[105] Node: '=' +-[108] Node: 'userId' \-[127] Node: ':' \-[108] Node: 'userId'
在antlr的語法樹中,使用BaseAST作為一個語法樹節點的抽象,在BaseAST儲存中,其資料結構更加類似一個連結串列。
protected BaseAST down;
protected BaseAST right;
BaseAST中down代表其在樹結構中的子節點,而right則代表跟他從屬一個父節點的兄弟節點。在語法樹中,父節點的down只指向邏輯順序的第一個子節點。
承接上文,當QueryTranslatorImpl中呼叫parse()方法中,當語法解析器parser呼叫statement正式開始語法解析。
從語法檔案hql.g中可以看到,所有hql解析的起點是statement規則。
statement
: ( updateStatement | deleteStatement | selectStatement | insertStatement ) (EOF!)
;
從這裡可以看到,語法解析器會依次從下面四個規則一次嘗試去選擇匹配,四個規則也正好對應了資料庫操作的增刪改查。
這裡再次放上等待作為例子等待解析的hql語句。
update com.tydhot.eninty.User set userName=:userName where userId=:userId
我們可以看到,首先會嘗試去選擇匹配updateStatement規則,如程式碼所示。
try { // for error handling
{
switch ( LA(1)) {
case UPDATE:
{
updateStatement();
astFactory.addASTChild(currentAST, returnAST);
break;
}
case DELETE:
{
deleteStatement();
astFactory.addASTChild(currentAST, returnAST);
break;
}
case EOF:
case FROM:
case GROUP:
case ORDER:
case SELECT:
case WHERE:
{
selectStatement();
astFactory.addASTChild(currentAST, returnAST);
break;
}
case INSERT:
{
insertStatement();
astFactory.addASTChild(currentAST, returnAST);
break;
}
default:
{
throw new NoViableAltException(LT(1), getFilename());
}
}
在上方的程式碼中,會從上一篇文章所提到的詞法解析器去提取所等到解析的hql語句的第一個詞,在這裡,如果是update關鍵字開頭,自然會開始進入updateStatement規則的匹配。
updateStatement
: UPDATE^ (optionalVersioned)?
optionalFromTokenFromClause
setClause
(whereClause)?
;
從語法檔案的配置還是可以看出,只有在第一個在字元匹配到了update才會繼續進行下面的匹配。接下來是根據這個語法規則antlr編譯的程式碼。
match(UPDATE);
{
switch ( LA(1)) {
case VERSIONED:
{
AST tmp2_AST = null;
tmp2_AST = astFactory.create(LT(1));
astFactory.addASTChild(currentAST, tmp2_AST);
match(VERSIONED);
break;
}
case FROM:
case IDENT:
{
break;
}
default:
{
throw new NoViableAltException(LT(1), getFilename());
}
}
}
optionalFromTokenFromClause();
astFactory.addASTChild(currentAST, returnAST);
setClause();
astFactory.addASTChild(currentAST, returnAST);
{
switch ( LA(1)) {
case WHERE:
{
whereClause();
astFactory.addASTChild(currentAST, returnAST);
break;
}
case EOF:
{
break;
}
default:
{
throw new NoViableAltException(LT(1), getFilename());
}
}
}
updateStatement_AST = (AST)currentAST.root;
可以看到,在取出第一個詞之後會通過match()確認關鍵字update的確認無誤,並生成第一個update節點作為當前樹的根節點,之後判斷下一個詞是否匹配版本關鍵字,對應語法檔案的optionalVersion,當然根據語法檔案中的正則顯示該語句塊可加可不加,那麼自然程式碼中對於沒有匹配到這一關鍵字,也沒有什麼別的操作。
而對於我們的例子語句
update com.tydhot.eninty.User set userName=:userName where userId=:userId
自然也沒有影響,會繼續往下匹配下一個語法規則,下一步則會匹配optionalFromTokenClause規則,程式碼中也會進入optionalFromTokenClause()程式碼段。
optionalFromTokenFromClause!
: (FROM!)? f:path (a:asAlias)? {
AST #range = #([RANGE, "RANGE"], #f, #a);
#optionalFromTokenFromClause = #([FROM, "FROM"], #range);
}
;
optionalFromTokenClause的語法規則就顯得稍微複雜。編譯後的程式碼如下。
switch ( LA(1)) {
case FROM:
{
match(FROM);
break;
}
case IDENT:
{
break;
}
default:
{
throw new NoViableAltException(LT(1), getFilename());
}
}
}
path();
f_AST = (AST)returnAST;
{
switch ( LA(1)) {
case AS:
case IDENT:
{
asAlias();
a_AST = (AST)returnAST;
break;
}
case EOF:
case SET:
case WHERE:
{
break;
}
default:
{
throw new NoViableAltException(LT(1), getFilename());
}
}
}
optionalFromTokenFromClause_AST = (AST)currentAST.root;
AST range = (AST)astFactory.make( (new ASTArray(3)).add(astFactory.create(RANGE,"RANGE")).add(f_AST).add(a_AST));
optionalFromTokenFromClause_AST = (AST)astFactory.make( (new ASTArray(2)).add(astFactory.create(FROM,"FROM")).add(range));
currentAST.root = optionalFromTokenFromClause_AST;
currentAST.child = optionalFromTokenFromClause_AST!=null &&optionalFromTokenFromClause_AST.getFirstChild()!=null ?
optionalFromTokenFromClause_AST.getFirstChild() : optionalFromTokenFromClause_AST;
currentAST.advanceChildToEnd();
程式碼比較長,根據語法規則,首先會嘗試匹配from關鍵字,當然無論有沒有匹配到都不會影響下面的操作。
接下來會對下一個部分強制進行path規則的匹配,也會進入程式碼的path()方法。
path
: identifier ( DOT^ { weakKeywords(); } identifier )*
;
Path的規則就顯得很簡單,首先強制要求必須存在一個常量,也就是說update語句的操作表物件至少要存在一個,但是在hql語句中可能存在以一個類名作為物件,就像下面這個例子。
update com.tydhot.eninty.User set userName=:userName where userId=:userId
那麼根據語法規則可以匹配若干個點‘ .’起頭的關鍵字進行匹配,下面是path()的程式碼。
identifier();
astFactory.addASTChild(currentAST, returnAST);
{
_loop315:
do {
if ((LA(1)==DOT)) {
AST tmp10_AST = null;
tmp10_AST = astFactory.create(LT(1));
astFactory.makeASTRoot(currentAST, tmp10_AST);
match(DOT);
weakKeywords();
identifier();
astFactory.addASTChild(currentAST, returnAST);
}
else {
break _loop315;
}
} while (true);
}
path_AST = (AST)currentAST.root;
在path()中,會強制第一個匹配一個常量,匹配到則會生成一個語法樹節點,之後會嘗試在迴圈中不斷去匹配點‘ .’,如果匹配到點,也會生成一個相應的語法樹節點,並將這個節點作為當前樹的根節點,其down則會指向剛才生成的常量節點,之後會繼續匹配下一個常量,並作為第一個常量節點right所指向的節點。再下一次迴圈中,如果繼續匹配到了點,則生成一個相應的新節點作為當前樹的根節點,其down指向第一個迴圈所產生的點對應的樹節點(也就是根節點),以此不斷迴圈產生節點,達到一次中序遍歷可以換元整個結果的目的,之前例子該部分生成的語法樹如下。
\-[15] Node: '.'
| +-[15] Node: '.'
| | +-[15] Node: '.'
| | | +-[108] Node: 'com'
| | | \-[108] Node: 'tydhot'
| | \-[108] Node: 'eninty'
| \-[108] Node: 'User'
從最左邊的com節點開始進行一次先序遍歷就可以還原整個結果。
在path()部分得到的這部分結果將會回到optionalFromTokenClause規則當中繼續作為語法樹的成員操作。
之後如果匹配到了AS關鍵字或者空格一個常量,那麼會繼續進入asAlias的規則匹配,但是根據語法規則,此處也是可有可無的部分,給出的例子即使沒有匹配到,也不會出現語法錯誤。
ST range = (AST)astFactory.make( (new ASTArray(3)).add(astFactory.create(RANGE,"RANGE")).add(f_AST).add(a_AST));
optionalFromTokenFromClause_AST = (AST)astFactory.make( (new ASTArray(2)).add(astFactory.create(FROM,"FROM")).add(range));
根據語法規則,產生的物件和其別名將會作為RANGE節點的子節點存放在語法樹中,而會繼續產生一個FROM節點繼續作為RANGE的父節點,最後在optionalFromTokenClause規則中產生的樹如下。
+-[22] Node: 'FROM'
| \-[90] Node: 'RANGE'
| \-[15] Node: '.'
| +-[15] Node: '.'
| | +-[15] Node: '.'
| | | +-[108] Node: 'com'
| | | \-[108] Node: 'tydhot'
| | \-[108] Node: 'eninty'
| \-[108] Node: 'User'
在完成了optionalFromTokenClause規則後,將得到的樹的根節點作為update的子節點,也是update的第一個子節點,被down所指向,而update的其他子節點將會被from節點所指向。
optionalFromTokenFromClause();
astFactory.addASTChild(currentAST, returnAST);
setClause();
astFactory.addASTChild(currentAST, returnAST);
回到update規則,接下來會強制進入setClause部分的語法匹配。接下來經過語法規則編譯的程式碼部分與上文類似,重點解析語法規則。
setClause
: (SET^ assignment (COMMA! assignment)*)
;
assignment
: stateField EQ^ newValue
;
stateField
: path
;
newValue
: concatenation
;
在setClause的語法規則中,根據正則,則會強制從set關鍵字進行匹配,接下來也強制至少存在一個等號表示式,複數的表示式可以通過逗號之間隔開,而在等號左邊,則支援path規則,也就是上文中通過.隔開常量的java物件形式的表達,而右邊則是一個concatenation規則,也就是被賦值的值。
concatenation
: additiveExpression
( c:CONCAT^ { #c.setType(EXPR_LIST); #c.setText("concatList"); }
additiveExpression
( CONCAT! additiveExpression )*
{ #concatenation = #([METHOD_CALL, "||"], #([IDENT, "concat"]), #c ); } )?
;
賦值語句分為五級,第4級也就是優先度最低的通過|隔開.
additiveExpression
: multiplyExpression ( ( PLUS^ | MINUS^ ) multiplyExpression )*
;
第3級則是根據加減分隔,根據先序遍歷的性質,這裡也將會在接下來的乘除操作之後進行加減。
multiplyExpression
: unaryExpression ( ( STAR^ | DIV^ | MOD^ ) unaryExpression )*
;
第2級也就是乘除,優先順序高於加減。
unaryExpression
: MINUS^ {#MINUS.setType(UNARY_MINUS);} unaryExpression
| PLUS^ {#PLUS.setType(UNARY_PLUS);} unaryExpression
| caseExpression
| quantifiedExpression
| atom
;
caseExpression
// NOTE : the unaryExpression rule contains the subQuery rule
: simpleCaseStatement
| searchedCaseStatement
;
simpleCaseStatement
: CASE^ unaryExpression (simpleCaseWhenClause)+ (elseClause)? END! {
#simpleCaseStatement.setType(CASE2);
}
;
simpleCaseWhenClause
: (WHEN^ unaryExpression THEN! unaryExpression)
;
elseClause
: (ELSE^ unaryExpression)
;
searchedCaseStatement
: CASE^ (searchedCaseWhenClause)+ (elseClause)? END!
;
searchedCaseWhenClause
: (WHEN^ logicalExpression THEN! unaryExpression)
;
quantifiedExpression
: ( SOME^ | EXISTS^ | ALL^ | ANY^ )
( identifier | collectionExpr | (OPEN! ( subQuery ) CLOSE!) )
;
第一級則是一些邏輯操作,優先度是除了常量和數字之外最高的。
atom
: primaryExpression
(
DOT^ identifier
( options { greedy=true; } :
( op:OPEN^ {#op.setType(METHOD_CALL);} exprList CLOSE! ) )?
| lb:OPEN_BRACKET^ {#lb.setType(INDEX_OP);} expression CLOSE_BRACKET!
)*
;
// level 0 - the basic element of an expression
primaryExpression
: { validateSoftKeyword("function") && LA(2) == OPEN && LA(3) == QUOTED_STRING }? jpaFunctionSyntax
| { validateSoftKeyword("cast") && LA(2) == OPEN }? castFunction
| identPrimary ( options {greedy=true;} : DOT^ "class" )?
| constant
| parameter
| OPEN! (expressionOrVector | subQuery) CLOSE!
;
第0級也就是一些常量和數字,是所有詞中,被處理優先順序最高的,這些也將在serClause規則中依次被處理成為語法樹中的節點。
我們的例子在經過set規則後,生成的樹的根節點將會被之前的from部分的right所指向,代表同為update的節點的子節點,在語句的順序上,也在其兄弟節點from節點的之後,生成的語法樹會成為如下所示。
\-[50] Node: 'update'
+-[22] Node: 'FROM'
| \-[90] Node: 'RANGE'
| \-[15] Node: '.'
| +-[15] Node: '.'
| | +-[15] Node: '.'
| | | +-[108] Node: 'com'
| | | \-[108] Node: 'tydhot'
| | \-[108] Node: 'eninty'
| \-[108] Node: 'User'
+-[46] Node: 'set'
| \-[105] Node: '='
| +-[108] Node: 'userName'
| \-[127] Node: ':'
| \-[108] Node: 'userName'
最後根據我們一開始的語法規則,where關鍵字的匹配是可有可無的,但是我們的例子給出了where 關鍵字,所以同樣的也要相應的繼續whereClause規則的匹配。
whereClause
: WHERE^ logicalExpression
;
ogicalExpression
: expression
;
// Main expression rule
expression
: logicalOrExpression
;
// level 7 - OR
logicalOrExpression
: logicalAndExpression ( OR^ logicalAndExpression )*
;
// level 6 - AND, NOT
logicalAndExpression
: negatedExpression ( AND^ negatedExpression )*
;
// NOT nodes aren't generated. Instead, the operator in the sub-tree will be
// negated, if possible. Expressions without a NOT parent are passed through.
negatedExpression!
{ weakKeywords(); } // Weak keywords can appear in an expression, so look ahead.
: NOT^ x:negatedExpression { #negatedExpression = negateNode(#x); }
| y:equalityExpression { #negatedExpression = #y; }
;
equalityExpression
: x:relationalExpression (
( EQ^
| is:IS^ { #is.setType(EQ); } (NOT! { #is.setType(NE); } )?
| NE^
| ne:SQL_NE^ { #ne.setType(NE); }
) y:relationalExpression)* {
// Post process the equality expression to clean up 'is null', etc.
#equalityExpression = processEqualityExpression(#equalityExpression);
}
;
Where的語法規則,在之前set的5級基礎上又添加了三級,分別是第七級的與,第六級的是非,和第五級的邏輯等式集合規則。
經過上述的語法規則後,生成的語法樹加入到最後的結果如下。
\-[50] Node: 'update'
+-[22] Node: 'FROM'
| \-[90] Node: 'RANGE'
| \-[15] Node: '.'
| +-[15] Node: '.'
| | +-[15] Node: '.'
| | | +-[108] Node: 'com'
| | | \-[108] Node: 'tydhot'
| | \-[108] Node: 'eninty'
| \-[108] Node: 'User'
+-[46] Node: 'set'
| \-[105] Node: '='
| +-[108] Node: 'userName'
| \-[127] Node: ':'
| \-[108] Node: 'userName'
\-[52] Node: 'where'
\-[105] Node: '='
+-[108] Node: 'userId'
\-[127] Node: ':'
\-[108] Node: 'userId'
在語法樹中,關鍵字部分下的子樹通過中序遍歷,都可以得到例子中的子句,最後將得到的字句根據關鍵字的順序組合,就能得到例子中的hql語句,這個語法樹,也將被用來作為轉換sql語句的依據。