MyBatis?SqlSource源碼示例解析
正文
MyBatis版本:3.5.12。
本篇講從mybatis的角度分析SqlSource。在xml中sql可能是帶?的預(yù)處理語句,也可能是帶$或者動(dòng)態(tài)標(biāo)簽的動(dòng)態(tài)語句,也可能是這兩者的混合語句。
SqlSource設(shè)計(jì)的目標(biāo)就是封裝xml的crud節(jié)點(diǎn),使得mybatis運(yùn)行過程中可以直接通過SqlSource獲取xml節(jié)點(diǎn)中解析后的SQL。
簡單的示意圖就是

接下來我們先來介紹幾個(gè)基礎(chǔ)的組件,正是這些組件構(gòu)成的SqlSource
SqlNode
mybatis提供了這么9種動(dòng)態(tài)節(jié)點(diǎn):
- trim
- where
- set
- foreach
- if
- choose
- when
- otherwise
- bind
每一種節(jié)點(diǎn)是一個(gè)SqlNode,并且每個(gè)動(dòng)態(tài)節(jié)點(diǎn)都分別對應(yīng)了一個(gè)XxxSqlNode的實(shí)現(xiàn)類。SqlNode是一個(gè)接口,該接口就代表mybatis的動(dòng)態(tài)節(jié)點(diǎn)。

接下來我們來用一個(gè)案例分析mybatis是如何把一個(gè)<select>節(jié)點(diǎn)解析為一個(gè)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>
它會(huì)被解析成如下這樣一顆SqlNode樹

樹的根節(jié)點(diǎn)都是MixedSqlNode,MixedSqlNode類其中有一個(gè)屬性private final List<SqlNode> contents;專門存放標(biāo)簽下所有的子節(jié)點(diǎn)解析成的SqlNode
該標(biāo)簽的的第一部分就是select * from user;這段文本既不包含標(biāo)簽,也不包含$等表達(dá)式,它就屬于靜態(tài)文本,會(huì)被解析成StaticTextSqlNode
- 然后與接下來是一個(gè)wehre標(biāo)簽,它會(huì)被解析為
WhereSqlNode - whhre標(biāo)簽中有兩個(gè)if標(biāo)簽,這兩個(gè)if標(biāo)簽會(huì)被解析為兩個(gè)
IfSqlNode加入到WhereSqlNode中 - 第一個(gè)if標(biāo)簽中的文本不包含
$會(huì)被解析成StaticTextSqlNode(沒錯(cuò),即使它有#符,它不屬于靜態(tài)文本哦。只有包含$才算動(dòng)態(tài)節(jié)點(diǎn)) - 而第二個(gè)if標(biāo)簽中的文本包含
$會(huì)被解析成TextSqlNode
看明白了xml文件中一個(gè)標(biāo)簽是如何由這些SqlNode是組成的。接下來我們嘮一嘮SqlNode接口的定義
SqlNode接口定義
public interface SqlNode {
boolean apply(DynamicContext context);
}
SqlNode接口定義非常簡單,只有一個(gè)apply方法,方法的參數(shù)是DynamicContext,DynamicContext可以看作是一個(gè)sql上下文,它其中維護(hù)了一個(gè)StringBuilder sql字段。這個(gè)字段就是用來記錄整個(gè)<select>節(jié)點(diǎn)解析過后的SQL語句的。
mybatis會(huì)在解析過程中把select標(biāo)簽解析為如上分析的一棵樹MixedSqlNode然后就會(huì)遞歸遍歷這些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中有一個(gè)List字段,該字段存儲(chǔ)的是樹的葉子節(jié)點(diǎn),在這個(gè)示例中,List字段中應(yīng)該由兩個(gè)SqlNode
- 第一個(gè)是標(biāo)識(shí)靜態(tài)文本的
StaticTextSqlNode,它其中封裝的select * from user文本。 - 第二個(gè)SqlNode是
WhereSqlNode它其中封裝的文本是
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="age != null">
and age > ${age}
</if>
</where>
而WhereSqlNode類中也還有一個(gè)List屬性,封裝了兩個(gè)if節(jié)點(diǎn),這里就不展開說了,我們只需要知道,所有的SqlNode都會(huì)遞歸執(zhí)行apply方法,而apply方法只做了一件事——那就是把SqlNode節(jié)點(diǎn)中的文本經(jīng)過一系列規(guī)則解析過后(通常就是刪除標(biāo)簽,刪除無用的and|or,刪除無用的,等),返回可執(zhí)行SQL的片段,這些SQL片段最終都會(huì)以如下方法把sql片段拼接,
context.appendSql(text);
最終形成一個(gè)完整的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方法后存儲(chǔ)在DynamicContext中的sql就會(huì)被賦值給該字段。sql字段其實(shí)就是類似于select * from user where id = ?這樣的字符串, - parameterObject:用戶傳入的屬性,用于給sql字段的
?賦值 - additionalParameters: bind標(biāo)簽中綁定的值會(huì)存儲(chǔ)在此
- metaParameters:additionalParameters的元類型
還記得開篇我們說的目標(biāo)嗎?我貼過來再看一遍
SqlSource設(shè)計(jì)的目標(biāo)就是封裝xml的crud節(jié)點(diǎn),使得mybatis運(yùn)行過程中可以直接通過SqlSource獲取xml節(jié)點(diǎn)中解析后的SQL。
簡單的示意圖就是

那么有了BoundSql,實(shí)現(xiàn)這個(gè)目標(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是一個(gè)接口,其中只提供了一個(gè)方法 getBoundSql 。該方法只有一個(gè)參數(shù)Object parameterObject,這個(gè)參數(shù)就是用戶傳入的查詢參數(shù)。SqlSource的繼承體系如下

- DynamicSqlSource:動(dòng)態(tài)SQL節(jié)點(diǎn)會(huì)被解析為該對象,那怎么判斷xml文件中的節(jié)點(diǎn)是否是動(dòng)態(tài)的呢?滿足如下兩個(gè)條件的任何一個(gè)就算是動(dòng)態(tài)節(jié)點(diǎn)。一是包含
$占位符的表達(dá)式,比如select * from user where id = ${id}。二是包含9種動(dòng)態(tài)標(biāo)簽中的任何一個(gè)(trim set wehre if foreach等9個(gè)。前文有說)。注意只包含#占位符表達(dá)式的語句不會(huì)被解析成動(dòng)態(tài)標(biāo)簽。 - ProviderSqlSource:注解定義的SQL
- RawSqlSource:不是DynamicSqlSource,就會(huì)被解析為RawSqlSource
- . StaticSqlSource:靜態(tài)文本SQL其中不包含任何
$和動(dòng)態(tài)標(biāo)簽。DynamicSqlSource和RawSqlSource最終都會(huì)被解析為StaticSqlSource - . VelocitySqlSource:暫且忽略(不在本文討論范圍)
SqlSource解析時(shí)機(jī)
至此SqlSource的組成部分我們都已經(jīng)清楚了,那么XML的節(jié)點(diǎn)在何時(shí)被解析為SqlSource的呢?
答案是在mybatis啟動(dòng)時(shí),會(huì)加載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);
}
}
該方法解析一個(gè)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);
}
}
}
該方法會(huì)遍歷xml中的所有select|insert|update|delete節(jié)點(diǎn)并解析。其中l(wèi)ist標(biāo)識(shí)所有select|insert|update|delete節(jié)點(diǎn)的結(jié)合。接下來來看parseStatementNode這個(gè)方法,它用來解析單個(gè)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的屬性存儲(chǔ)在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就是在這個(gè)方法中創(chuàng)建SqlSource對象,他首先會(huì)調(diào)用parseDynamicTags方法來解析下節(jié)點(diǎn)是否是動(dòng)態(tài)節(jié)點(diǎn),它的解析過程就是看節(jié)點(diǎn)是否包含動(dòng)態(tài)標(biāo)簽或包含$占位符,如果滿足任意一個(gè)條件它就會(huì)被解析為動(dòng)態(tài)標(biāo)簽,并創(chuàng)建DynamicSqlSource對象,否則創(chuàng)建RawSqlSource對象
SqlSource調(diào)用時(shí)機(jī)
mybatis需要的是可以執(zhí)行的SQL,而通過SqlSource我們可以獲取BoundSql進(jìn)而獲取BoundSql中的sql字段(該字段就是可執(zhí)行語句)。所以其調(diào)用時(shí)機(jī)是在mybatis進(jìn)行查詢數(shù)據(jù)庫的時(shí)候——調(diào)用SqlSource#getBoundSql
具體代碼處是Executor執(zhí)行query方法的時(shí)候調(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生成時(shí)會(huì)被存儲(chǔ)在MappedStatement對象當(dāng)中,所以這里自然也是通過MappedStatement對象來使用SqlSource獲取BoundSql。這樣在mybatis真正調(diào)用JDBC查詢數(shù)據(jù)庫的時(shí)候就可以通過BoundSql拿到可執(zhí)行語句啦
總結(jié)
- SqlSource封裝了XML中的
select|insert|update|delete節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)都會(huì)被解析為MixedSqlNode,可以看作是一棵樹,其中包含許多子節(jié)點(diǎn)嵌套 - 只包含
#的sql不算動(dòng)態(tài)節(jié)點(diǎn),只有包含動(dòng)態(tài)標(biāo)簽或者$占位符才算是動(dòng)態(tài)節(jié)點(diǎn) - BoundSql中包含了可執(zhí)行sql
本文只是粗略的介紹了SqlSource,只能帶你粗略的了解下mybatis的組件結(jié)構(gòu)。其中SqlSource如何獲取BoundSql對象,以及節(jié)點(diǎn)到底是如何被解析的,比如if標(biāo)簽是如何進(jìn)行判斷的 等。讀者在理解了這些概念后再閱讀源碼會(huì)容易很多。
以上就是MyBatis SqlSource源碼示例解析的詳細(xì)內(nèi)容,更多關(guān)于MyBatis SqlSource源碼解析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring JPA聯(lián)表查詢之OneToMany源碼解析
這篇文章主要為大家介紹了Spring JPA聯(lián)表查詢之OneToMany源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
spring @schedule注解如何動(dòng)態(tài)配置時(shí)間間隔
這篇文章主要介紹了spring @schedule注解如何動(dòng)態(tài)配置時(shí)間間隔,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Maven 多模塊父子工程的實(shí)現(xiàn)(含Spring Boot示例)
這篇文章主要介紹了Maven 多模塊父子工程的實(shí)現(xiàn)(含Spring Boot示例),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
Java設(shè)計(jì)模塊系列之書店管理系統(tǒng)單機(jī)版(二)
這篇文章主要為大家詳細(xì)介紹了Java單機(jī)版的書店管理系統(tǒng)設(shè)計(jì)模塊和思想第二章,感興趣的小伙伴們可以參考一下2016-08-08
Springboot整合Mybatispuls的實(shí)例詳解
這篇文章主要介紹了Springboot整合Mybatispuls的相關(guān)資料,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11

