需要了解更多可以參考這篇文章http://www.itdecent.cn/p/af52bdb8106b
動(dòng)態(tài)SQL
說(shuō)到動(dòng)態(tài)SQL,就不得不提Script,Java作為一個(gè)靜態(tài)語(yǔ)音,代碼需要先編譯,然后再運(yùn)行,雖然帶來(lái)了效率,但是卻損失了靈活性。
Spring為此還專門提供了一套SpEL用來(lái)封裝Java腳本語(yǔ)言API
在MyBatis中,也支持動(dòng)態(tài)SQL,想要將簡(jiǎn)單的String字符串編譯成能運(yùn)行的代碼,需要其他的庫(kù)的支持,MyBatis內(nèi)部使用的是OGNL庫(kù)。
在OgnlCache中,是MyBatis對(duì)OGNL的簡(jiǎn)單封裝:
public static Object getValue(String expression, Object root) {
try {
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
主要便是增加了一層緩存。
有了上面的基礎(chǔ),我們就可以通過(guò)需求,來(lái)了解實(shí)現(xiàn)了:
在MyBatis中,動(dòng)態(tài)SQL標(biāo)簽有如下幾個(gè):
if :通過(guò)條件判斷執(zhí)行SQL
choose :通過(guò)switch選擇一條執(zhí)行SQL 一般和when / otherwise一起使用
trim : 簡(jiǎn)單加工SQL,比如去除頭尾的逗號(hào)等,同類的還有where / set
foreach : 遍歷容器,將遍歷的結(jié)果拼接成SQL
bind : 通過(guò)OGNL表達(dá)式獲取指定的值,并綁定到環(huán)境變量中
簡(jiǎn)單的使用方式如下:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
可以看到,動(dòng)態(tài)SQL的關(guān)鍵就是獲取title的值,然后執(zhí)行test對(duì)應(yīng)的表達(dá)式,最后根據(jù)結(jié)果拼接SQL
最后也是比較重要的一點(diǎn)就是,MyBatis的動(dòng)態(tài)SQL標(biāo)簽是可以嵌套使用的:
比如:
<update id="update" parameterType="User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
<where> 1=1
<if test="id != null">
and id = ${id}
</if>
</where>
</update>
這樣的結(jié)構(gòu),就像是一顆樹,需要層層遍歷處理。
組合模式
前面說(shuō)到了MyBatis處理動(dòng)態(tài)SQL的需求,需要處理嵌套的標(biāo)簽。
而這個(gè),恰好符合組合模式的解決場(chǎng)景。
在MyBatis中,處理動(dòng)態(tài)SQL的關(guān)鍵類如下:
SqlNode : 用來(lái)表示動(dòng)態(tài)標(biāo)簽的相關(guān)信息
NodeHandler : 用來(lái)處理SqlNode其他信息的類
DynamicContext : 用來(lái)保存處理整個(gè)標(biāo)簽過(guò)程中,解析出來(lái)的信息,主要元素為StringBuilder
SqlSource : 用來(lái)表示XML中SQL的信息,MyBatis中,動(dòng)態(tài)SQL最終都會(huì)通過(guò)SqlSource表示
SqlNode接口的定義如下:
public interface SqlNode {
//處理目前的信息,并將處理完畢的信息追加到DynamicContext 中
boolean apply(DynamicContext context);
}
接下來(lái)從MyBaits創(chuàng)建以及使用SqlSource上來(lái)分析動(dòng)態(tài)SQL的使用:
創(chuàng)建SqlSource的代碼如下:
XMLScriptBuilder#parseScriptNode()
public SqlSource parseScriptNode() {
//創(chuàng)建組合模式中的根節(jié)點(diǎn)
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//如果發(fā)現(xiàn)是動(dòng)態(tài)節(jié)點(diǎn),則創(chuàng)建DynamicSqlSource
//反之創(chuàng)建RawSqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
接著看parseDynamicTags()
protected MixedSqlNode parseDynamicTags(XNode node) {
//使用list保存所有sqlNode
List<SqlNode> contents = new ArrayList<>();
//遍歷所有的子節(jié)點(diǎn)
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
//如果節(jié)點(diǎn)是Text節(jié)點(diǎn),則使用TextSqlNode處理
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//包含${},則需要額外處理
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
}
//如果是一個(gè)節(jié)點(diǎn)
else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getNode().getNodeName();
//通過(guò)節(jié)點(diǎn)名獲取節(jié)點(diǎn)的處理類
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//處理節(jié)點(diǎn)
handler.handleNode(child, contents);
isDynamic = true;
}
}
//返回根節(jié)點(diǎn)
return new MixedSqlNode(contents);
}
TextSqlNode作用之一便是檢測(cè)SQL中是否包含${},如果包含,則判斷為Dynamic。
TextSqlNode的作用主要和#{xxx}類似,但是實(shí)現(xiàn)方式不同,#{xxx}底層是通過(guò)JDBC#ParperedStatement的setXXX方法設(shè)置參數(shù),具有防止SQL注入的功能,而TextSqlNode則是直接替換的String,不會(huì)做任何的SQL處理,因此一般不建議使用。
接下來(lái)再看MixedSqlNode,它的作用是作為根節(jié)點(diǎ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) {
//遍歷調(diào)用apply方法
contents.forEach(node -> node.apply(context));
return true;
}
}
可以看見,非常簡(jiǎn)單,就是用來(lái)遍歷所有子節(jié)點(diǎn),分別調(diào)用apply()方法。
接下來(lái)我們看看其他標(biāo)簽的使用:
IfSqlNode
首先看ifSqlNode的創(chuàng)建:
IfSqlHandler#handleNode()
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//加載子節(jié)點(diǎn)信息
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//獲取Test表達(dá)式信息
String test = nodeToHandle.getStringAttribute("test");
//將信息傳入`ifSqlNode`
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
可以看到,這里IfNode也充當(dāng)了一個(gè)根節(jié)點(diǎn),里面包含了其子節(jié)點(diǎn)信息。
這里可以大概猜想處理,IfSqlNode會(huì)通過(guò)OGNL執(zhí)行test的內(nèi)容,如果true,則執(zhí)行后面的SqlNode,否則跳過(guò)
IfSqlNode#apply()
@Override
public boolean apply(DynamicContext context) {
//通過(guò)OGNL判斷test的值
if (evaluator.evaluateBoolean(test, context.getBindings())) {
//如果為`true`則遍歷子節(jié)點(diǎn)執(zhí)行
contents.apply(context);
return true;
}
//否則跳過(guò)
return false;
}
可以看到和前面的推理相符合
ChooseNode
ChooseHandler#handleNode()
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<>();
List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
//遍歷子節(jié)點(diǎn),生成對(duì)應(yīng)的SqlNode 將其保存在各個(gè)對(duì)應(yīng)的容器中
//whenSqlNode 的處理和IfNode的處理相同
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
//驗(yàn)證otherwise的數(shù)量的合法性,只能有一個(gè)otherwise節(jié)點(diǎn)
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
//生成對(duì)應(yīng)的ChooseSqlNode
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
這里就可以猜想到ChooseNode對(duì)Node的處理的,應(yīng)該是遍歷所有的ifNode,然后當(dāng)遇到符合條件的,邊處理后續(xù)的Node,否則執(zhí)行otherwise
ChooseSqlNode#apply()
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
TrimNode
TrimeNode是對(duì)SQL語(yǔ)句進(jìn)行加工。
其包含3個(gè)屬性:
prefix : 需要添加的前綴
suffix : 需要添加的尾綴
prefixOverrides : 當(dāng)SQL 是以此標(biāo)志開頭的時(shí)候,需要移除的開頭的內(nèi)容
suffixOverrides : 當(dāng)SQL 是以此標(biāo)志結(jié)尾的時(shí)候,需要移除的結(jié)尾的內(nèi)容
現(xiàn)在舉個(gè)例子:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
</trim>
</select>
可以看到,trim會(huì)自動(dòng)為SQL 增加Where前綴,同時(shí)當(dāng)state為null的時(shí)候,SQL會(huì)以AND開頭,此時(shí)trim標(biāo)簽便會(huì)自動(dòng)將AND刪除。
同理,SET可能會(huì)遇到,結(jié)尾,只需要使用suffixOverrides 刪除結(jié)尾即可,這里不再敘述。
接下來(lái)查看Trim的源碼:
TrimHandler#handleNode()
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//獲取子節(jié)點(diǎn)
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//獲取前綴
String prefix = nodeToHandle.getStringAttribute("prefix");
//獲取前綴需要?jiǎng)h除的內(nèi)容
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
//獲取尾綴
String suffix = nodeToHandle.getStringAttribute("suffix");
//獲取尾綴需要?jiǎng)h除的內(nèi)容
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
//創(chuàng)建`TrimSqlNode`
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
這里可以看到,沒有其他的處理,只是獲取了屬性然后初始化
TrimSqlNode#apply()
@Override
public boolean apply(DynamicContext context) {
//創(chuàng)建FilteredDynamicContext對(duì)象
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
//獲取子元素的處理結(jié)果
boolean result = contents.apply(filteredDynamicContext);
//整體拼接SQL
filteredDynamicContext.applyAll();
return result;
}
這里出現(xiàn)了一個(gè)新的對(duì)象:FilteredDynamicContext,FilteredDynamicContext繼承自DynamicContext,其相對(duì)于DynamicContext僅僅多了一個(gè)新的方法:applyAll(),
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
//添加前綴
applyPrefix(sqlBuffer, trimmedUppercaseSql);
//添加后綴
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
其中,applyPrefix()方法會(huì)檢查SQL是否startWith()需要?jiǎng)h除的元素,如果有,則刪除。
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
//如果SQL以toRemove開頭,則刪除
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
ForEachNode
foreach節(jié)點(diǎn)的元素很多:
item: 遍歷的時(shí)候所獲取的元素的具體的值,類似for(String item:list )中的item,對(duì)于Map,item對(duì)應(yīng)為value
index : 遍歷的時(shí)候所遍歷的索引,類似for(int i=0;i<10;i++) 中的i,對(duì)于Map,index對(duì)應(yīng)為key
collection : 需要遍歷的集合的參數(shù)名字,如果指定了@Param,則名字為@Param指定的名字,否則如果只有一個(gè)參數(shù),且這個(gè)參數(shù)是集合的話,需要使用MyBatis包裝的名字:
對(duì)于Collection : 名字為collection
對(duì)于List : 名字為list
對(duì)于數(shù)組:名字為array
相關(guān)代碼如下:
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<>();
map.put("array", object);
return map;
}
return object;
}
open : 類似TrimNode中的prefix
close : 類似TrimNode中的suffix
separator : 每個(gè)SQL 的分割符
使用方式如下:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
以上元素沒有默認(rèn)值,當(dāng)沒有設(shè)置的時(shí)候,MyBatis便不會(huì)設(shè)置相關(guān)的值,對(duì)于open或close,我們一般都會(huì)自己加上括號(hào),所以有時(shí)候可以不設(shè)置。
接下來(lái)我們查看MyBatis的foreach的源碼:
ForEachNode的初始化代碼沒什么好看的,就是簡(jiǎn)單的獲取相關(guān)的屬性,然后初始化。我們直接看其apply()方法。
public boolean apply(DynamicContext context) {
//準(zhǔn)備添加綁定
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
//追加Open符號(hào)
applyOpen(context);
//記錄索引,用來(lái)賦值給`index`
int i = 0;
//調(diào)用`OGNL`的迭代器
for (Object o : iterable) {
//PrefixedContext繼承自DynamicContext,主要是增加了分隔符
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
//對(duì)于Map key會(huì)綁定到index , value會(huì)綁定到item上
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
//實(shí)時(shí)綁定i到index上
applyIndex(context, i, uniqueNumber);
//實(shí)時(shí)綁定具體的值到item上
applyItem(context, o, uniqueNumber);
}
//生成對(duì)應(yīng)的占位符,并綁定相關(guān)的值#{__frch_item_1}等
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
//追加結(jié)尾符
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
BindNode
bind節(jié)點(diǎn)可以方便的運(yùn)行OGNL表達(dá)式,并將結(jié)果綁定到指定的變量。
使用方法如下:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
一般可以內(nèi)置使用的元素為_parameter表示現(xiàn)在的參數(shù),以及_databaseId,表示現(xiàn)在的database id
對(duì)于BindNode,對(duì)應(yīng)的是VarDeclSqlNode,具體的代碼這里不再細(xì)看,大概就是使用OGNL獲取具體的值,比較簡(jiǎn)單。
對(duì)于動(dòng)態(tài)SQL的節(jié)點(diǎn)對(duì)應(yīng)的類,我們就分析完了,可以看到SqlNode完美的應(yīng)用了組合模式,每個(gè)SqlNode都保存了其子節(jié)點(diǎn)下面的節(jié)點(diǎn),執(zhí)行下來(lái)便像是一顆樹的遞歸。
當(dāng)然,SqlNode的使用僅僅是動(dòng)態(tài)SQL的一部分,但是它確實(shí)動(dòng)態(tài)SQL的核心部分。