MyBatis?SqlSource源碼示例解析
正文
MyBatis版本:3.5.12。
本篇講從mybatis的角度分析SqlSource。在xml中sql可能是帶?
的預(yù)處理語句,也可能是帶$
或者動態(tài)標(biāo)簽的動態(tài)語句,也可能是這兩者的混合語句。
SqlSource設(shè)計的目標(biāo)就是封裝xml的crud節(jié)點(diǎn),使得mybatis運(yùn)行過程中可以直接通過SqlSource獲取xml節(jié)點(diǎn)中解析后的SQL。
簡單的示意圖就是
接下來我們先來介紹幾個基礎(chǔ)的組件,正是這些組件構(gòu)成的SqlSource
SqlNode
mybatis提供了這么9種動態(tài)節(jié)點(diǎn):
- trim
- where
- set
- foreach
- if
- choose
- when
- otherwise
- bind
每一種節(jié)點(diǎn)是一個SqlNode
,并且每個動態(tài)節(jié)點(diǎn)都分別對應(yīng)了一個XxxSqlNode
的實(shí)現(xiàn)類。SqlNode
是一個接口,該接口就代表mybatis的動態(tài)節(jié)點(diǎn)。
接下來我們來用一個案例分析mybatis是如何把一個<select>
節(jié)點(diǎn)解析為一個SqlNode對象的(update/insert/delete原理一樣)。示例如下
<select id="selectById"> select * from user <where> <if test="id != null"> and id = #{id} </if> <if test="age != null"> and age > ${age} </if> </where> </select>
它會被解析成如下這樣一顆SqlNode樹
樹的根節(jié)點(diǎn)都是MixedSqlNode
,MixedSqlNode
類其中有一個屬性private final List<SqlNode> contents;
專門存放標(biāo)簽下所有的子節(jié)點(diǎn)解析成的SqlNode
該標(biāo)簽的的第一部分就是select * from user
;這段文本既不包含標(biāo)簽,也不包含$
等表達(dá)式,它就屬于靜態(tài)文本,會被解析成StaticTextSqlNode
- 然后與接下來是一個wehre標(biāo)簽,它會被解析為
WhereSqlNode
- whhre標(biāo)簽中有兩個if標(biāo)簽,這兩個if標(biāo)簽會被解析為兩個
IfSqlNode
加入到WhereSqlNode
中 - 第一個if標(biāo)簽中的文本不包含
$
會被解析成StaticTextSqlNode
(沒錯,即使它有#
符,它不屬于靜態(tài)文本哦。只有包含$
才算動態(tài)節(jié)點(diǎn)) - 而第二個if標(biāo)簽中的文本包含
$
會被解析成TextSqlNode
看明白了xml文件中一個標(biāo)簽是如何由這些SqlNode是組成的。接下來我們嘮一嘮SqlNode
接口的定義
SqlNode接口定義
public interface SqlNode { boolean apply(DynamicContext context); }
SqlNode接口定義非常簡單,只有一個apply
方法,方法的參數(shù)是DynamicContext
,DynamicContext
可以看作是一個sql上下文,它其中維護(hù)了一個StringBuilder sql
字段。這個字段就是用來記錄整個<select>
節(jié)點(diǎn)解析過后的SQL語句的。
mybatis會在解析過程中把select標(biāo)簽解析為如上分析的一棵樹MixedSqlNode
然后就會遞歸遍歷這些SqlNode
并調(diào)用他們的apply方法,調(diào)用apply
方法實(shí)際上就是把標(biāo)簽解析后的sql片段拼接到了context中的sql字段。最后只需要調(diào)用context.getSql
方法就可以獲得可執(zhí)行SQL了。而一切都從根節(jié)點(diǎn)的apply方法說起,MixedSqlNode的源碼如下
public class MixedSqlNode implements SqlNode { private final List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; } @Override public boolean apply(DynamicContext context) { contents.forEach(node -> node.apply(context)); return true; } }
可以發(fā)現(xiàn)MixedSqlNode中有一個List字段,該字段存儲的是樹的葉子節(jié)點(diǎn),在這個示例中,List字段中應(yīng)該由兩個SqlNode
- 第一個是標(biāo)識靜態(tài)文本的
StaticTextSqlNode
,它其中封裝的select * from user
文本。 - 第二個SqlNode是
WhereSqlNode
它其中封裝的文本是
<where> <if test="id != null"> and id = #{id} </if> <if test="age != null"> and age > ${age} </if> </where>
而WhereSqlNode
類中也還有一個List屬性,封裝了兩個if節(jié)點(diǎn),這里就不展開說了,我們只需要知道,所有的SqlNode都會遞歸執(zhí)行apply方法,而apply方法只做了一件事——那就是把SqlNode節(jié)點(diǎn)中的文本經(jīng)過一系列規(guī)則解析過后(通常就是刪除標(biāo)簽,刪除無用的and|or
,刪除無用的,
等),返回可執(zhí)行SQL的片段,這些SQL片段最終都會以如下方法把sql片段拼接,
context.appendSql(text);
最終形成一個完整的SQL:select * from user where id = 1
(age條件沒成立)
BoundSql
知道了什么是SqlNode之后,我們再來看BoundSql
,BoundSql
內(nèi)部封裝了可執(zhí)行SQL,先來看下BoundSql
的重要字段
public class BoundSql { private final String sql; private final List<ParameterMapping> parameterMappings; private final Object parameterObject; private final Map<String, Object> additionalParameters; private final MetaObject metaParameters; }
- sql:上小節(jié)說到的
SqlNode
調(diào)用完apply
方法后存儲在DynamicContext
中的sql就會被賦值給該字段。sql字段其實(shí)就是類似于select * from user where id = ?
這樣的字符串, - parameterObject:用戶傳入的屬性,用于給sql字段的
?
賦值 - additionalParameters: bind標(biāo)簽中綁定的值會存儲在此
- metaParameters:additionalParameters的元類型
還記得開篇我們說的目標(biāo)嗎?我貼過來再看一遍
SqlSource設(shè)計的目標(biāo)就是封裝xml的crud節(jié)點(diǎn),使得mybatis運(yùn)行過程中可以直接通過SqlSource獲取xml節(jié)點(diǎn)中解析后的SQL。
簡單的示意圖就是
那么有了BoundSql,實(shí)現(xiàn)這個目標(biāo)是不是就很容易了。我們只需要獲取BoundSql對象,然后再調(diào)用BoundSql#getSql
方法就能獲取到可執(zhí)行Sql了。
SqlSource
為了完成開篇說的SqlSource的目標(biāo),我們現(xiàn)在迫切想要做的就是獲取BoundSql對象。剛好SqlSource接口的定義如下
public interface SqlSource { BoundSql getBoundSql(Object parameterObject); }
SqlSource是一個接口,其中只提供了一個方法 getBoundSql
。該方法只有一個參數(shù)Object parameterObject
,這個參數(shù)就是用戶傳入的查詢參數(shù)。SqlSource的繼承體系如下
- DynamicSqlSource:動態(tài)SQL節(jié)點(diǎn)會被解析為該對象,那怎么判斷xml文件中的節(jié)點(diǎn)是否是動態(tài)的呢?滿足如下兩個條件的任何一個就算是動態(tài)節(jié)點(diǎn)。一是包含
$
占位符的表達(dá)式,比如select * from user where id = ${id}
。二是包含9種動態(tài)標(biāo)簽中的任何一個(trim set wehre if foreach等9個。前文有說)。注意只包含#
占位符表達(dá)式的語句不會被解析成動態(tài)標(biāo)簽。 - ProviderSqlSource:注解定義的SQL
- RawSqlSource:不是DynamicSqlSource,就會被解析為RawSqlSource
- . StaticSqlSource:靜態(tài)文本SQL其中不包含任何
$
和動態(tài)標(biāo)簽。DynamicSqlSource和RawSqlSource最終都會被解析為StaticSqlSource - . VelocitySqlSource:暫且忽略(不在本文討論范圍)
SqlSource解析時機(jī)
至此SqlSource的組成部分我們都已經(jīng)清楚了,那么XML的節(jié)點(diǎn)在何時被解析為SqlSource的呢?
答案是在mybatis啟動時,會加載xml文件并進(jìn)行解析。相關(guān)流程如下
- XMLMapperBuilder#configurationElement,
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
該方法解析一個xml文件中所有的節(jié)點(diǎn):namespace、cache-ref、cache等,其中解析select|insert|update|delete
節(jié)點(diǎn)的方法是buildStatementFromContext
- XMLMapperBuilder#buildStatementFromContext
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
該方法會遍歷xml中的所有select|insert|update|delete
節(jié)點(diǎn)并解析。其中l(wèi)ist標(biāo)識所有select|insert|update|delete
節(jié)點(diǎn)的結(jié)合。接下來來看parseStatementNode這個方法,它用來解析單個select|insert|update|delete
節(jié)點(diǎn)
- XMLStatementBuilder#parseStatementNode
public void parseStatementNode() { // 省略解析 id flushCache useCache SelectKey resultType等屬性的過程 // 創(chuàng)建SqlSource對象,也就是解析xml的crud標(biāo)簽,封裝成SqlSource對象,然后再把SqlSource對象存入MS對象中 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect); }
可以看到SqlSource在此被創(chuàng)建了,并且最后作為MappedStatement的屬性存儲在MappedStatement對象中。這里我們著重關(guān)心SqlSource的創(chuàng)建過程,它是在createSqlSource
方法完成的
- XMLLanguageDriver#createSqlSource
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); }
XMLLanguageDriver又委托XMLScriptBuilder解析,接下來我們看XMLScriptBuilder#parseScriptNode方法
- XMLScriptBuilder#parseScriptNode
public SqlSource parseScriptNode() { MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }
mybatis就是在這個方法中創(chuàng)建SqlSource對象,他首先會調(diào)用parseDynamicTags方法來解析下節(jié)點(diǎn)是否是動態(tài)節(jié)點(diǎn),它的解析過程就是看節(jié)點(diǎn)是否包含動態(tài)標(biāo)簽或包含$
占位符,如果滿足任意一個條件它就會被解析為動態(tài)標(biāo)簽,并創(chuàng)建DynamicSqlSource
對象,否則創(chuàng)建RawSqlSource
對象
SqlSource調(diào)用時機(jī)
mybatis需要的是可以執(zhí)行的SQL,而通過SqlSource我們可以獲取BoundSql進(jìn)而獲取BoundSql中的sql字段(該字段就是可執(zhí)行語句)。所以其調(diào)用時機(jī)是在mybatis進(jìn)行查詢數(shù)據(jù)庫的時候——調(diào)用SqlSource#getBoundSql
具體代碼處是Executor執(zhí)行query方法的時候調(diào)用,源碼在BaseExecutor中,BaseExecutor#query
代碼如下
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); return boundSql; }
我們前文說過,SqlSource生成時會被存儲在MappedStatement對象當(dāng)中,所以這里自然也是通過MappedStatement對象來使用SqlSource獲取BoundSql。這樣在mybatis真正調(diào)用JDBC查詢數(shù)據(jù)庫的時候就可以通過BoundSql拿到可執(zhí)行語句啦
總結(jié)
- SqlSource封裝了XML中的
select|insert|update|delete
節(jié)點(diǎn),每個節(jié)點(diǎn)都會被解析為MixedSqlNode
,可以看作是一棵樹,其中包含許多子節(jié)點(diǎn)嵌套 - 只包含
#
的sql不算動態(tài)節(jié)點(diǎn),只有包含動態(tài)標(biāo)簽或者$
占位符才算是動態(tài)節(jié)點(diǎn) - BoundSql中包含了可執(zhí)行sql
本文只是粗略的介紹了SqlSource,只能帶你粗略的了解下mybatis的組件結(jié)構(gòu)。其中SqlSource如何獲取BoundSql對象,以及節(jié)點(diǎn)到底是如何被解析的,比如if
標(biāo)簽是如何進(jìn)行判斷的 等。讀者在理解了這些概念后再閱讀源碼會容易很多。
以上就是MyBatis SqlSource源碼示例解析的詳細(xì)內(nèi)容,更多關(guān)于MyBatis SqlSource源碼解析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java List集合返回值去掉中括號(''[ ]'')的操作
這篇文章主要介紹了Java List集合返回值去掉中括號('[ ]')的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08解決dubbo啟動報服務(wù)注冊失敗Failed?to?register?dubbo
這篇文章主要介紹了解決dubbo啟動報服務(wù)注冊失敗Failed?to?register?dubbo問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12Java中Array、List、ArrayList的區(qū)別及說明
這篇文章主要介紹了Java中Array、List、ArrayList的區(qū)別及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07java實(shí)現(xiàn)ArrayList根據(jù)存儲對象排序功能示例
這篇文章主要介紹了java實(shí)現(xiàn)ArrayList根據(jù)存儲對象排序功能,結(jié)合實(shí)例形式分析了java針對ArrayList的相關(guān)運(yùn)算、排序操作技巧,需要的朋友可以參考下2018-01-01