詳解MyBatis的動態(tài)SQL實現(xiàn)原理
前言
MyBatis版本:3.5.6
正文
一. XML文檔中的節(jié)點概念
在分析MyBatis如何支持SQL語句之前,本小節(jié)先分析XML文檔中的節(jié)點概念。XML文檔中的每個成分都是一個節(jié)點,DOM對XML節(jié)點的規(guī)定如下所示。
- 整個文檔是一個文檔節(jié)點;
- 每個XML標(biāo)簽是一個元素節(jié)點;
- 包含在元素節(jié)點中的文本是文本節(jié)點。
以一個XML文檔進(jìn)行說明,如下所示。
<provinces>
<province name="四川">
<capital>成都</capital>
</province>
<province name="湖北">
<capital>武漢</capital>
</province>
</provinces>如上所示,整個XML文檔是一個文檔節(jié)點,這個文檔節(jié)點有一個子節(jié)點,就是<provinces>元素節(jié)點,<provinces>元素節(jié)點有五個子節(jié)點,分別是:
- 文本節(jié)點;
- <province>元素節(jié)點;
- 文本節(jié)點,
- <province>元素節(jié)點;
- 文本節(jié)點。
注意,在<provinces>元素節(jié)點的子節(jié)點中的文本節(jié)點的文本值均是\n,表示換行符。
同樣,<province>元素節(jié)點有三個子節(jié)點,分別是:
- 文本節(jié)點;
- <capital>元素節(jié)點;
- 文本節(jié)點。
這里的文本節(jié)點的文本值也是\n。
然后<capital>元素節(jié)點只有一個子節(jié)點,為一個文本節(jié)點。節(jié)點的子節(jié)點之間互為兄弟節(jié)點,例如<provinces>元素的五個子節(jié)點之間互為兄弟節(jié)點,name為"四川"的<province>元素節(jié)點的上一個兄弟節(jié)點為文本節(jié)點,下一個兄弟節(jié)點也為文本節(jié)點。
二. 動態(tài)SQL解析流程說明
整體的一個解析流程如下所示。

也就是寫在映射文件中的一條SQL,會最終被解析為DynamicSqlSource或者RawSqlSource,前者表示動態(tài)SQL,后者表示靜態(tài)SQL。
上圖中的MixedSqlNode,其通常的包含關(guān)系可以由下圖定義。

也就是映射文件中定義一條SQL語句的CRUD標(biāo)簽里的各種子元素,均會被解析為一個SqlNode,比如包含了${}的文本,會被解析為TextSqlNode,不包含${}的文本,會被解析為StaticTextSqlNode,<choose>標(biāo)簽會被解析為ChooseSqlNode等,同時又因為<choose>標(biāo)簽中會再有<when>和<otherwise>子標(biāo)簽,所以ChooseSqlNode中又會持有這些子標(biāo)簽的SqlNode。
所以一條SQL最終就是由這條SQL對應(yīng)的CRUD標(biāo)簽解析成的各種SqlNode組合而成。
三. MyBatis解析動態(tài)SQL源碼分析
在詳解MyBatis加載映射文件和動態(tài)代理的實現(xiàn)中已經(jīng)知道,在XMLStatementBuilder的parseStatementNode() 方法中,會解析映射文件中的<select>,<insert>,<update>和<delete>標(biāo)簽(后續(xù)統(tǒng)一稱為CURD標(biāo)簽),并生成MappedStatement然后緩存到Configuration中。
CURD標(biāo)簽的解析由XMLLanguageDriver完成,每個標(biāo)簽解析之后會生成一個SqlSource,可以理解為SQL語句,本小節(jié)將對XMLLanguageDriver如何完成CURD標(biāo)簽的解析進(jìn)行討論。
XMLLanguageDriver創(chuàng)建SqlSource的createSqlSource() 方法如下所示。
public SqlSource createSqlSource(Configuration configuration,
XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(
configuration, script, parameterType);
return builder.parseScriptNode();
}如上所示,createSqlSource() 方法的入?yún)⒅校?strong>XNode就是CURD標(biāo)簽對應(yīng)的節(jié)點,在createSqlSource() 方法中先是創(chuàng)建了一個XMLScriptBuilder,然后通過XMLScriptBuilder來生成SqlSource。先看一下XMLScriptBuilder的構(gòu)造方法,如下所示。
public XMLScriptBuilder(Configuration configuration, XNode context,
Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}在XMLScriptBuilder的構(gòu)造方法中,主要是將CURD標(biāo)簽對應(yīng)的節(jié)點緩存起來,然后初始化nodeHandlerMap,nodeHandlerMap中存放著處理MyBatis提供的支持動態(tài)SQL的標(biāo)簽的處理器,initNodeHandlerMap() 方法如下所示。
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}現(xiàn)在分析XMLScriptBuilder的parseScriptNode() 方法,該方法會創(chuàng)建SqlSource,如下所示。
public SqlSource parseScriptNode() {
// 解析動態(tài)標(biāo)簽
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 創(chuàng)建DynamicSqlSource并返回
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 創(chuàng)建RawSqlSource并返回
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}在XMLScriptBuilder的parseScriptNode() 方法中,會根據(jù)XMLScriptBuilder中的isDynamic屬性判斷是創(chuàng)建DynamicSqlSource還是RawSqlSource,在這里暫時不分析DynamicSqlSource與RawSqlSource的區(qū)別,但是可以推測在parseDynamicTags() 方法中會改變isDynamic屬性的值,即在parseDynamicTags() 方法中會根據(jù)CURD標(biāo)簽的節(jié)點生成一個MixedSqlNode,同時還會改變isDynamic屬性的值以指示當(dāng)前CURD標(biāo)簽中的SQL語句是否是動態(tài)的。
MixedSqlNode是什么,isDynamic屬性值在什么情況下會變?yōu)?strong>true,帶著這些疑問,繼續(xù)看parseDynamicTags() 方法,如下所示。
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 獲取節(jié)點的子節(jié)點
NodeList children = node.getNode().getChildNodes();
// 遍歷所有子節(jié)點
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
// 子節(jié)點為文本節(jié)點
String data = child.getStringBody("");
// 基于文本節(jié)點的值并創(chuàng)建TextSqlNode
TextSqlNode textSqlNode = new TextSqlNode(data);
// isDynamic()方法可以判斷文本節(jié)點值是否有${}占位符
if (textSqlNode.isDynamic()) {
// 文本節(jié)點值有${}占位符
// 添加TextSqlNode到集合中
contents.add(textSqlNode);
// 設(shè)置isDynamic為true
isDynamic = true;
} else {
// 文本節(jié)點值沒有占位符
// 創(chuàng)建StaticTextSqlNode并添加到集合中
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 子節(jié)點為元素節(jié)點
// CURD節(jié)點的子節(jié)點中的元素節(jié)點只可能為<if>,<foreach>等動態(tài)Sql標(biāo)簽節(jié)點
String nodeName = child.getNode().getNodeName();
// 根據(jù)動態(tài)Sql標(biāo)簽節(jié)點的名稱獲取對應(yīng)的處理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 處理動態(tài)Sql標(biāo)簽節(jié)點
handler.handleNode(child, contents);
// 設(shè)置isDynamic為true
isDynamic = true;
}
}
// 創(chuàng)建MixedSqlNode
return new MixedSqlNode(contents);
}按照正常執(zhí)行流程調(diào)用parseDynamicTags() 時,入?yún)⑹?strong>CURD標(biāo)簽節(jié)點,此時會遍歷CURD標(biāo)簽節(jié)點的所有子節(jié)點,基于每個子節(jié)點都會創(chuàng)建一個SqlNode然后添加到SqlNode集合contents中,最后將contents作為入?yún)?chuàng)建MixedSqlNode并返回。
SqlNode是一個接口,在parseDynamicTags() 方法中,可以知道,TextSqlNode實現(xiàn)了SqlNode接口,StaticTextSqlNode實現(xiàn)了SqlNode接口,所以當(dāng)節(jié)點的子節(jié)點是文本節(jié)點時,如果文本值包含有${}占位符,則創(chuàng)建TextSqlNode添加到contents中并設(shè)置isDynamic為true,如果文本值不包含${}占位符,則創(chuàng)建StaticTextSqlNode并添加到contents中。
如果CURD標(biāo)簽節(jié)點的子節(jié)點是元素節(jié)點,由于CURD標(biāo)簽節(jié)點的元素節(jié)點只可能為<if>,<foreach>等動態(tài)SQL標(biāo)簽節(jié)點,所以直接會設(shè)置isDynamic為true,同時還會調(diào)用動態(tài)SQL標(biāo)簽節(jié)點對應(yīng)的處理器來生成SqlNode并添加到contents中。這里以<if>標(biāo)簽節(jié)點對應(yīng)的處理器的handleNode() 方法為例進(jìn)行說明,如下所示。
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 遞歸調(diào)用parseDynamicTags()解析<if>標(biāo)簽節(jié)點
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
// 創(chuàng)建IfSqlNode
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
// 將IfSqlNode添加到contents中
targetContents.add(ifSqlNode);
}在<if>標(biāo)簽節(jié)點對應(yīng)的處理器的handleNode() 方法中,遞歸的調(diào)用了parseDynamicTags() 方法來解析<if>標(biāo)簽節(jié)點,例如<where>,<foreach>等標(biāo)簽節(jié)點對應(yīng)的處理器的handleNode() 方法中也會遞歸調(diào)用parseDynamicTags() 方法,這是因為這些動態(tài)SQL標(biāo)簽是可以嵌套使用的,比如<where>標(biāo)簽節(jié)點的子節(jié)點可以為<if>標(biāo)簽節(jié)點。通過上面的handleNode() 方法,大致可以知道MixedSqlNode和IfSqlNode也實現(xiàn)了SqlNode接口,下面看一下MixedSqlNode和IfSqlNode的實現(xiàn),如下所示。
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;
}
}
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}其實到這里已經(jīng)逐漸清晰明了了,按照正常執(zhí)行流程調(diào)用parseDynamicTags() 方法時,是為了將CURD標(biāo)簽節(jié)點的所有子節(jié)點根據(jù)子節(jié)點類型生成不同的SqlNode并放在MixedSqlNode中,然后將MixedSqlNode返回,但是CURD標(biāo)簽節(jié)點的子節(jié)點中如果存在動態(tài)SQL標(biāo)簽節(jié)點,因為這些動態(tài)SQL標(biāo)簽節(jié)點也會有子節(jié)點,所以此時會遞歸的調(diào)用parseDynamicTags() 方法,以解析動態(tài)SQL標(biāo)簽節(jié)點的子節(jié)點,同樣會將這些子節(jié)點生成SqlNode并放在MixedSqlNode中然后將MixedSqlNode返回,遞歸調(diào)用parseDynamicTags() 方法時得到的MixedSqlNode會保存在動態(tài)SQL標(biāo)簽節(jié)點對應(yīng)的SqlNode中,比如IfSqlNode中就會將遞歸調(diào)用parseDynamicTags() 生成的MixedSqlNode賦值給IfSqlNode的contents字段。
不同的SqlNode都是可以包含彼此的,這是組合設(shè)計模式的應(yīng)用,SqlNode之間的關(guān)系如下所示。

SqlNode接口定義了一個方法,如下所示。
public interface SqlNode {
boolean apply(DynamicContext context);
}每個SqlNode的apply() 方法中,除了實現(xiàn)自己本身的邏輯外,還會調(diào)用自己所持有的所有SqlNode的apply() 方法,最終逐層調(diào)用下去,所有SqlNode的apply() 方法均會被執(zhí)行。
四. DynamicSqlSource和RawSqlSource源碼分析
回到XMLScriptBuilder的parseScriptNode() 方法,該方法中會調(diào)用parseDynamicTags() 方法以解析CURD標(biāo)簽節(jié)點并得到MixedSqlNode,MixedSqlNode中含有被解析的CURD標(biāo)簽節(jié)點的所有子節(jié)點對應(yīng)的SqlNode,最后會基于MixedSqlNode創(chuàng)建DynamicSqlSource或者RawSqlSource,如果CURD標(biāo)簽中含有動態(tài)SQL標(biāo)簽或者SQL語句中含有${}占位符,則創(chuàng)建DynamicSqlSource,否則創(chuàng)建RawSqlSource。下面分別對DynamicSqlSource和RawSqlSource的實現(xiàn)進(jìn)行分析。
1. DynamicSqlSource源碼分析
DynamicSqlSource的實現(xiàn)如下所示。
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
// 構(gòu)造函數(shù)只是進(jìn)行了簡單的賦值操作
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 調(diào)用SqlNode的apply()方法完成Sql語句的生成
rootSqlNode.apply(context);
// SqlSourceBuilder可以將Sql語句中的#{}占位符替換為?
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 將Sql語句中的#{}占位符替換為?,并生成一個StaticSqlSource
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// StaticSqlSource中保存有動態(tài)生成好的Sql語句,并且#{}占位符全部替換成了?
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 生成有序參數(shù)映射列表
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}DynamicSqlSource的構(gòu)造函數(shù)只是進(jìn)行了簡單的賦值操作,重點在于其getBoundSql() 方法,在getBoundSql() 方法中,先是調(diào)用DynamicSqlSource中的SqlNode的apply() 方法以完成動態(tài)SQL語句的生成,此時生成的SQL語句中的占位符(如果有的話)為#{},然后再調(diào)用SqlSourceBuilder的parse() 方法將SQL語句中的占位符從#{}替換為?并基于替換占位符后的SQL語句生成一個StaticSqlSource并返回,這里可以看一下StaticSqlSource的實現(xiàn),如下所示。
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql,
List<ParameterMapping> parameterMappings) {
// 構(gòu)造函數(shù)只是進(jìn)行簡單的賦值操作
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 基于Sql語句創(chuàng)建一個BoundSql并返回
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}所以分析到這里,可以知道DynamicSqlSource的getBoundSql() 方法實際上會完成動態(tài)SQL語句的生成和#{}占位符替換,然后基于生成好的SQL語句創(chuàng)建BoundSql并返回。BoundSql對象的類圖如下所示。

實際上,MyBatis中執(zhí)行SQL語句時,如果映射文件中的SQL使用到了動態(tài)SQL標(biāo)簽,那么MyBatis中的Executor(執(zhí)行器,后續(xù)文章中會進(jìn)行介紹)會調(diào)用MappedStatement的getBoundSql() 方法,然后在MappedStatement的getBoundSql() 方法中又會調(diào)用DynamicSqlSource的getBoundSql() 方法,所以MyBatis中的動態(tài)SQL語句會在這條語句實際要執(zhí)行時才會生成。
2. RawSqlSource源碼分析
現(xiàn)在看一下RawSqlSource的實現(xiàn),如下所示。
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
// 先調(diào)用getSql()方法獲取Sql語句
// 然后再執(zhí)行構(gòu)造函數(shù)
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 將Sql語句中的#{}占位符替換為?,生成一個StaticSqlSource并賦值給sqlSource
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 實際是調(diào)用StaticSqlSource的getBoundSql()方法
return sqlSource.getBoundSql(parameterObject);
}
}RawSqlSource會在構(gòu)造函數(shù)中就將SQL語句生成好并替換#{}占位符,在SQL語句實際要執(zhí)行時,就直接將生成好的SQL語句返回。所以MyBatis中,靜態(tài)SQL語句的執(zhí)行通常要快于動態(tài)SQL語句的執(zhí)行,這在RawSqlSource類的注釋中也有提及,如下所示。
Static SqlSource. It is faster than {@link DynamicSqlSource} because mappings are calculated during startup.
總結(jié)
MyBatis會為映射文件中的每個CURD標(biāo)簽節(jié)點里的SQL語句生成一個SqlSource:
- 如果是靜態(tài)SQL語句,那么會生成RawSqlSource;
- 如果是動態(tài)SQL語句,則會生成DynamicSqlSource。
MyBatis在生成SqlSource時,會為CURD標(biāo)簽節(jié)點的每個子節(jié)點都生成一個SqlNode,無論子節(jié)點是文本值節(jié)點還是動態(tài)SQL元素節(jié)點,最終所有子節(jié)點對應(yīng)的SqlNode都會放在SqlSource中以供生成SQL語句使用。
如果是靜態(tài)SQL語句,那么在創(chuàng)建RawSqlSource時就會使用SqlNode完成SQL語句的生成以及將SQL語句中的#{}占位符替換為?,然后保存在RawSqlSource中,等到這條靜態(tài)SQL語句要被執(zhí)行時,就直接返回這條靜態(tài)SQL語句。
如果是動態(tài)SQL語句,在創(chuàng)建DynamicSqlSource時只會簡單的將SqlNode保存下來,等到這條動態(tài)SQL語句要被執(zhí)行時,才會使用SqlNode完成SQL語句的生成以及將SQL語句中的#{}占位符替換為?,最后返回SQL語句。
所以MyBatis中,靜態(tài)SQL語句的獲取要快于動態(tài)SQL語句。
以上就是詳解MyBatis的動態(tài)SQL實現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于MyBatis 動態(tài)SQL實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
- MyBatis之關(guān)于動態(tài)SQL解讀
- MyBatis動態(tài)SQL、模糊查詢與結(jié)果映射操作過程
- MybatisPlus使用Mybatis的XML的動態(tài)SQL的功能實現(xiàn)多表查詢
- Mybatis使用注解實現(xiàn)復(fù)雜動態(tài)SQL的方法詳解
- Mybatis使用XML實現(xiàn)動態(tài)sql的示例代碼
- 詳解MyBatis特性之動態(tài)SQL
- MyBatis映射文件中的動態(tài)SQL實例詳解
- MyBatis中的XML實現(xiàn)和動態(tài)SQL實現(xiàn)示例詳解
- Mybatis動態(tài)Sql標(biāo)簽使用小結(jié)
- Mybatis之動態(tài)SQL使用小結(jié)(全網(wǎng)最新)
- MyBatis實現(xiàn)動態(tài)SQL的方法
相關(guān)文章
jmeter設(shè)置全局變量與正則表達(dá)式提取器過程圖解
這篇文章主要介紹了jmeter設(shè)置全局變量與正則表達(dá)式提取器過程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-10-10
SpringBoot整合Hashids實現(xiàn)數(shù)據(jù)ID加密隱藏的全過程
這篇文章主要為大家詳細(xì)介紹了SpringBoot整合Hashids實現(xiàn)數(shù)據(jù)ID加密隱藏的全過程,文中的示例代碼講解詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01
Java中public關(guān)鍵字用法詳細(xì)講解
這篇文章主要給大家介紹了關(guān)于Java中public關(guān)鍵字用法的相關(guān)資料,public關(guān)鍵字是和訪問權(quán)限相關(guān)的,它所修飾的方法對所有類都是可以訪問的,需要的朋友可以參考下2023-09-09
基于spring如何實現(xiàn)事件驅(qū)動實例代碼
這篇文章主要給大家介紹了關(guān)于基于spring如何實現(xiàn)事件驅(qū)動的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用spring具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

