動態Sql介紹
動態 SQL 是 MyBatis 的強大特性之一。如果你使用過 JDBC 或其它類似的框架,你應該能理解根據不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號。利用動態 SQL,可以徹底擺脫這種痛苦。
使用動態 SQL 并非一件易事,但借助可用于任何 SQL 映射語句中的強大的動態 SQL 語言,MyBatis 顯著地提升了這一特性的易用性。
Mybatis動態解析里面有2個核心的類SqlNode、SqlSource、ExpressionEvaluator。Mybatis動態Sql使用分為2個部分:動態Sql解析、動態Sql拼接執行。
封裝SqlNode
SqlNode是在解析Xml文件的時候對動態Sql進行解析,并存在MappedStatement的sqlSource屬性中。對于嵌套動態Sql,mybatis用遞歸調用來進行解析。這塊東西個人覺得還是比較繞,所以這塊博主準備事例、源碼、執行結果一起講解。
Sql腳本分類
在Mybatis中Sql腳本分為2種類型:靜態Sql和動態Sql。下面我們通過具體的源碼來看下2者區分。
靜態Sql和動態Sql
靜態Sql說白了就沒有太任何判斷了解的Sql腳本。
//Select是查詢的一些屬性
SqlNode類結果體系
看mybatis代碼很多時候可以看到這種結構。每個SqlNode負責自己那塊功能。職責單一。SqlNode的核心方法apply就是通過ExpressionEvaluator來解析OGNL表達式數據的。接下來我們看看Mybatis是如何遞歸解析動態sql腳本的。
//解析Sql腳本節點 publicSqlSourceparseScriptNode(){ //解析靜態和動態腳本,并存在MixedSqlNode里面 //這行代碼很關鍵,后面我們會去分析parseDynamicTags這里就是一層一層遞歸調用該方法把Sql腳本生成MixedSqlNode對象。 MixedSqlNoderootSqlNode=parseDynamicTags(context); SqlSourcesqlSource=null; //是否為動態Sql if(isDynamic){ //動態Sql則生成DynamicSqlSource sqlSource=newDynamicSqlSource(configuration,rootSqlNode); }else{ //否則為靜態SqlSource sqlSource=newRawSqlSource(configuration,rootSqlNode,parameterType); } returnsqlSource; }
//Anhighlightedblock protectedMixedSqlNodeparseDynamicTags(XNodenode){ //創建個SqlNode,這個列表存了當前Sql腳本節點下的所有的SqlNode信息 Listcontents=newArrayList (); NodeListchildren=node.getNode().getChildNodes(); for(inti=0;iinSQLstatement."); } //調用對應的handler進行節點處理,遞歸調用就在這塊 handler.handleNode(child,contents); isDynamic=true; } } //創建MixedSqlNode returnnewMixedSqlNode(contents); }
//下面我們看下IfHandler是如何處理,IfHandler是XMLScriptBuilder的內部類 privateclassIfHandlerimplementsNodeHandler{ publicIfHandler(){ //PreventSyntheticAccess } //我們著重分析這個方法 @Override publicvoidhandleNode(XNodenodeToHandle,ListtargetContents){ //調用parseDynamicTags進行節點解析。這里就是遞歸,又調用了上面的方法。 MixedSqlNodemixedSqlNode=parseDynamicTags(nodeToHandle); //獲取if對應的表達式 Stringtest=nodeToHandle.getStringAttribute("test"); //創建IfSqlNode IfSqlNodeifSqlNode=newIfSqlNode(mixedSqlNode,test); targetContents.add(ifSqlNode); } }
下面我們根據Sql腳本和執行結果來分析。
//靜態Sql腳本和嵌套的動態Sql腳本
下面我們分析下執行結果:
上面遞歸結果已經用不通顏色標記了,大家自己看下。特別需要看下IfSqlNode的屬性。
動態Sql解析
動態Sql解析主要是執行數據庫操作的時候把動態Sql轉換成JDBC能識別的Sql腳本。Mybatis中主要是通過SqlSource來解析Sql腳本,替換成JDBC能識別的Sql腳本。我們先看下類圖。
SqlSource:提供了Sql解析的行為。
RawSqlSource:靜態Sql腳本的編譯,只生成一次StaticSqlSource。
DynamicSqlSource:每次調用都會生成StaticSqlSource。每次調用傳入參數可能不一樣。需要每次生成StaticSqlSource。
ProviderSqlSource:第三方腳本語言的集成。
FreeMarkerSqlSource:對FreeMarker的支持。
StaticSqlSource:StaticSqlSource只是對上面4中類型做了層封裝。博主沒有這個類會更清爽些。
我們這次主要對StaticSqlSource、RawSqlSource、和DynamicSqlSource進行分析。
StaticSqlSource
其實StaticSqlSource就是對其他幾種類型Sql處理器結果進行包裝。我們看下源碼。
//我們主要分析下getBoundSql publicclassStaticSqlSourceimplementsSqlSource{ privatefinalStringsql; privatefinalListparameterMappings; privatefinalConfigurationconfiguration; publicStaticSqlSource(Configurationconfiguration,Stringsql){ this(configuration,sql,null); } publicStaticSqlSource(Configurationconfiguration,Stringsql,List parameterMappings){ this.sql=sql; this.parameterMappings=parameterMappings; this.configuration=configuration; } //getBoundSql就是創建一個BoundSql對象。 @Override publicBoundSqlgetBoundSql(ObjectparameterObject){ returnnewBoundSql(configuration,sql,parameterMappings,parameterObject); } }
看完是不是非常簡單,其實有些代碼確實沒有我們想象中那么難。
RawSqlSource
//我們著重分析RawSqlSource方法 publicclassRawSqlSourceimplementsSqlSource{ privatefinalSqlSourcesqlSource; publicRawSqlSource(Configurationconfiguration,SqlNoderootSqlNode,Class>parameterType){ this(configuration,getSql(configuration,rootSqlNode),parameterType); } //這里實現了對靜態腳本的解析,所謂的靜態腳本解析就是把#{}解析成?靜態Sql解析是在解析Mapper.xml的時候執行的 publicRawSqlSource(Configurationconfiguration,Stringsql,Class>parameterType){ SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration); Class>clazz=parameterType==null?Object.class:parameterType; //通過調用SqlSourceBuilder的parse方法來解析Sql sqlSource=sqlSourceParser.parse(sql,clazz,newHashMap()); } privatestaticStringgetSql(Configurationconfiguration,SqlNoderootSqlNode){ DynamicContextcontext=newDynamicContext(configuration,null); rootSqlNode.apply(context); returncontext.getSql(); } @Override publicBoundSqlgetBoundSql(ObjectparameterObject){ returnsqlSource.getBoundSql(parameterObject); } }
下面我們來看下SqlSourceBuilder的parse方法
publicSqlSourceparse(StringoriginalSql,Class>parameterType,MapadditionalParameters){ ParameterMappingTokenHandlerhandler=newParameterMappingTokenHandler(configuration,parameterType,additionalParameters); //找到Sql腳本中#{}符號的腳本用?號進行替代。GenericTokenParser里面代碼比較復雜,博主也沒有研究。 //有興趣自己可以研究下。 GenericTokenParserparser=newGenericTokenParser("#{","}",handler); Stringsql=parser.parse(originalSql); returnnewStaticSqlSource(configuration,sql,handler.getParameterMappings()); }
DynamicSqlSource
動態Sql解析主要由DynamicSqlSource來完成。這里面又是通過遞歸調進行sql解析。我們還是延用上面的Sql給大家講解。
publicclassDynamicSqlSourceimplementsSqlSource{ privatefinalConfigurationconfiguration; privatefinalSqlNoderootSqlNode; publicDynamicSqlSource(Configurationconfiguration,SqlNoderootSqlNode){ this.configuration=configuration; this.rootSqlNode=rootSqlNode; } @Override publicBoundSqlgetBoundSql(ObjectparameterObject){ //動態Sql解析上下文 DynamicContextcontext=newDynamicContext(configuration,parameterObject); //rootSqlNode就是我們前面講解的,把動態Sql解析成SqlNode對象。外層為MixedSqlNode節點,節點存儲了 //節點下的所有子節點。里面遞歸調用并根據傳入參數的屬性檢查是否需要拼接sql rootSqlNode.apply(context); //這塊代碼和上面靜態Sql接代碼一致。 SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration); Class>parameterType=parameterObject==null?Object.class:parameterObject.getClass(); //把我們動態Sql中的#{}替換成? SqlSourcesqlSource=sqlSourceParser.parse(context.getSql(),parameterType,context.getBindings()); BoundSqlboundSql=sqlSource.getBoundSql(parameterObject); for(Map.Entryentry:context.getBindings().entrySet()){ boundSql.setAdditionalParameter(entry.getKey(),entry.getValue()); } returnboundSql; } }
動態Sql解析apply方法博主只根據場景介紹下MixedSqlNode和IfSqlNode的apply方法。其他有興趣自己去研究下。邏輯大體一致,實現有些區別。
publicclassMixedSqlNodeimplementsSqlNode{ privatefinalListcontents; publicMixedSqlNode(List contents){ this.contents=contents; } //獲取循環SqlNode列表的所有SqlNode,調用apply方法根據傳入參數和條件進行靜態sql的拼接。 //列表中的SqlNode可能是一個簡單的SqlNode對象,也可能是一個MixedSqlNode或者有更多的嵌套。 //博主的例子就是3個嵌套If查詢。根據博主的Sql腳本,這里直接會調用IfSqlNode的apply方法。 //我們接下來看下IfSqlNode是如何實現的。 @Override publicbooleanapply(DynamicContextcontext){ for(SqlNodesqlNode:contents){ sqlNode.apply(context); } returntrue; } }
IfSqlNode的apply
publicclassIfSqlNodeimplementsSqlNode{ //ExpressionEvaluator會調用ognl來對表達式進行解析 privatefinalExpressionEvaluatorevaluator; privatefinalStringtest; privatefinalSqlNodecontents; publicIfSqlNode(SqlNodecontents,Stringtest){ this.test=test; this.contents=contents; this.evaluator=newExpressionEvaluator(); } @Override publicbooleanapply(DynamicContextcontext){ //context.getBindings()里面就存儲這請求參數,這里是一個HashMap,OGNl里面代碼博主沒有研究。 //如果條件if成立,直接獲取contents中的SqlNode的apply方法進行動態腳本處理。 if(evaluator.evaluateBoolean(test,context.getBindings())){ contents.apply(context); returntrue; } returnfalse; } }
這塊代碼很多遞歸調用,博主自認為講的不太透徹,所以大家看完務必自己去調試下。
總結
Mybatis動態Sql從解析到執行分為2個過程下面對這個2個過程進行簡單總結。
1.動態Sql生成SqlNode信息,這個過程發生在對select、update等Sql語句解析過程。如果是靜態Sql直接會把#{}替換成?。
2.動態Sql解析在獲取BoundSql時候觸發。會調用SqlNode的apply進行Sql解析成靜態Sql,然后把#{}替換成?,并綁定ParameterMapping映射。
-
SQL
+關注
關注
1文章
762瀏覽量
44117 -
源碼
+關注
關注
8文章
639瀏覽量
29185 -
腳本
+關注
關注
1文章
389瀏覽量
14858
原文標題:Mybatis動態Sql處理
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論