1. 程式人生 > >hibernate中antlr對於hql生成抽象語法樹原始碼解析

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語句的依據。