您好,登錄后才能下訂單哦!
這篇文章主要講解了“Mybatis中SQL節(jié)點(diǎn)實(shí)例分析”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Mybatis中SQL節(jié)點(diǎn)實(shí)例分析”吧!
某天在完成項(xiàng)目中的一個(gè)小功能后進(jìn)行自測(cè)的時(shí)候,發(fā)現(xiàn)存在一個(gè)很奇怪的 bug --- 最終執(zhí)行的 SQL 與我所期望的 SQL 不一致,有一個(gè) if 分支在我不傳特定參數(shù)的情況下被拼接在最終的 SQL 上。
①定義在 XML 文件中的 SQL 語句
<select id="balanceByUserIds" parameterType="xxx.BalanceReqVO" resultType="xxx.Balance"> select * from balance <where> <if test="dataOrgCodes != null and dataOrgCodes.size > 0"> and data_org_code in <foreach collection="dataOrgCodes" open="(" separator="," close=")" item="dataOrgCode"> #{dataOrgCode} </foreach> </if> <if test="dataOrgCode != null and dataOrgCode != ''"> and data_org_code = #{dataOrgCode} </if> </where> </select>
②傳進(jìn)來的參數(shù)
{ "dataOrgCodes":["6","2"] }
③Mybatis 打印執(zhí)行的 SQL
SELECT * FROM balance WHERE data_org_code IN (?, ?) AND data_org_code = ?
打印的執(zhí)行參數(shù)
{ "dataOrgCodes":["6","2"] }
學(xué)過 Mybatis 的人應(yīng)該一樣就看出來了,這個(gè) SQL 不對(duì)勁,多了一些不該有的東西。按照我們的理解,最終的執(zhí)行的 SQL 應(yīng)該是
SELECT * FROM balance WHERE data_org_code IN (?, ?)
但 mybatis 執(zhí)行的 SQL 多了一點(diǎn)語句---AND data_org_code = ?
在出現(xiàn)這個(gè)問題后我反復(fù)進(jìn)行 debug,確定了自己傳進(jìn)來的參數(shù)沒有什么問題,也沒有什么攔截器添加多余的參數(shù)。
在確定編寫 XML 文件的 if 標(biāo)簽的內(nèi)容以及傳進(jìn)來的參數(shù)無誤后,排除了參數(shù)導(dǎo)致問題。那么除了這個(gè)可能外,問題就可能出現(xiàn)在 SQL 的解析上,也就是 SQL 的生成那里。那么我們定位到 SQL 的生成地方, DynamicSqlSource#getBoundSql(我們查詢的參數(shù)對(duì)象)方法
// Configuration是Mybatis核心類,rootSqlNode 根SQL節(jié)點(diǎn)是我們定義在XML中的SQL語句。 //(例如<select>rootSqlNode</sselect>, 標(biāo)簽中間的內(nèi)容就是 rootSqlNode) public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; } public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); .............................. BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; }
可以看到方法內(nèi)部顯示創(chuàng)建了一個(gè) DynamicContext,這個(gè)對(duì)象就是用于存儲(chǔ)動(dòng)態(tài)生成的 SQL。
(下面是省略了很多關(guān)于本次問題無關(guān)的代碼,只保留有關(guān)代碼)
public class DynamicContext { public static final String PARAMETER_OBJECT_KEY = "_parameter"; public static final String DATABASE_ID_KEY = "_databaseId"; // 存儲(chǔ)動(dòng)態(tài)生成的SQL,類似于 StringBuilder 的角色 private final StringJoiner sqlBuilder = new StringJoiner(" "); // 唯一編號(hào)值,會(huì)在生成最終SQL和參數(shù)值映射關(guān)系的時(shí)候用到 private int uniqueNumber = 0; // 拼接SQL public void appendSql(String sql) { sqlBuilder.add(sql); } // 獲取拼接好的SQL public String getSql() { return sqlBuilder.toString().trim(); } // 獲取唯一編號(hào),返回后進(jìn)行加一 public int getUniqueNumber() { return uniqueNumber++; } }
而下一句就是解析我們編寫的 SQL,完成 SQL 的拼接
rootSqlNode.apply(context)
這里的 rootSqlNode 是我們編寫在標(biāo)簽里的 SQL 內(nèi)容,包括<if>、<foreach>、<where>標(biāo)簽等內(nèi)容。
rootSqlNode 對(duì)象是 SqlNode 類型。其實(shí)這里的 SQL 語句被解析成類似于 HTML 的 DOM 節(jié)點(diǎn)的樹級(jí)結(jié)構(gòu),在本節(jié)的測(cè)試?yán)又薪Y(jié)構(gòu)類似如下(不完全正確,只做參考價(jià)值,表示 rootSqlNode 結(jié)構(gòu)類似于以下結(jié)構(gòu)):
<SqlNode> select * from balance <SqlNode> where <SqlNode> and data_org_code in <SqlNode> #{dataOrgCode} </SqlNode> </SqlNode> <SqlNode> and data_org_code = <SqlNode> #{dataOrgCode} </SqlNode> </SqlNode> </SqlNode> </SqlNode>
這個(gè) SqlNode 定義如下所示:
public interface SqlNode { boolean apply(DynamicContext context); }
里面的 apply 方法是用于評(píng)估是否把這個(gè) SqlNode 的內(nèi)容拼接到最終返回的 SQL 上的,不同類型的 SqlNode 有不同的實(shí)現(xiàn),例如我們本節(jié)相關(guān)的 SqlNode 類型就是為 IfSqlNode,對(duì)應(yīng)這我們寫的 SQL 語句的 if 標(biāo)簽,以及存儲(chǔ)最終的 sql 內(nèi)容的 StaticTextSqlNode 類型。
public class StaticTextSqlNode implements SqlNode { // 存儲(chǔ)我們寫的 sql // 類似于 and data_org_code in private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) { // 調(diào)用 DynamicContext 對(duì)象的 sqppendSql 方法拼接最終 sql context.appendSql(text); return true; } }
public class IfSqlNode implements SqlNode { // 評(píng)估器 private final ExpressionEvaluator evaluator; // if標(biāo)簽中用于判斷這個(gè)語句是否生效的 test 屬性值 // 這里對(duì)應(yīng)我們例子中的一個(gè)為 "dataOrgCodes != null and dataOrgCodes.size > 0" private final String test; // if標(biāo)簽中的內(nèi)容,如果if標(biāo)簽中不存在其他標(biāo)簽,那么這里的值就是StaticTextSqlNode類型的節(jié)點(diǎn) // StaticTextSqlNode 節(jié)點(diǎn)的 text 屬性就是我們最終需要拼接的 sql 語句 private final SqlNode contents; // contents 是我們定義在 if 標(biāo)簽里面的內(nèi)容, test 是 if 標(biāo)簽的屬性 test 定義的內(nèi)容 public IfSqlNode(SqlNode contents, String test) { this.test = test; this.contents = contents; this.evaluator = new ExpressionEvaluator(); } @Override public boolean apply(DynamicContext context) { // 使用評(píng)估器評(píng)估 if 標(biāo)簽中定義的 test 中的內(nèi)容是否為true if (evaluator.evaluateBoolean(test, context.getBindings())) { // 當(dāng)contents為StaticTextSqlNode類型的節(jié)點(diǎn)時(shí)候,就把 if 標(biāo)簽里的內(nèi)容拼接到 sql 上 // 否則繼續(xù)調(diào)用方法 apply(相當(dāng)于遞歸調(diào)用,知道找到最下面的內(nèi)容節(jié)點(diǎn)) contents.apply(context); return true; } return false; } }
我們可以看到這里的
evaluator.evaluateBoolean(test, context.getBindings())
這個(gè)評(píng)估方法是通過把 test 語句內(nèi)容和 我們傳進(jìn)來的參數(shù)解析出來的 Map 進(jìn)行比對(duì),如果我們的參數(shù)中存在值,且值得內(nèi)容符合 test 語句的判斷,則進(jìn)行 sql 語句的拼接。例如本次例子中的
<if test="userIds != null and userIds.size > 0"> and data_org_code in <foreach collection="dataOrgCodes" open="(" separator="," close=")" item="dataOrgCode"> #{dataOrgCode} </foreach> </if>
以及我們傳進(jìn)來的參數(shù)進(jìn)行比對(duì)
{ "dataOrgCodes":["6","2"] }
可以看得出來參數(shù)與 test 語句 "dataOrgCodes!= null and dataOrgCodes.size > 0" 比較是返回 true 的。
根據(jù)上面的執(zhí)行步驟可以知道,我們的 bug 的產(chǎn)生是在
evaluator.evaluateBoolean(test, context.getBindings()) 這一步產(chǎn)生的。也就是在 context.getBindings() 中存在滿足 dataOrgCode != null and dataOrgCode != '' 的屬性。debug 驗(yàn)證以下可知
可以看得出來,存儲(chǔ)參數(shù)映射的 Map 出現(xiàn)了 dataOrgCode 的屬性,但是我們傳遞進(jìn)來的屬性只有 dataOrgCodes 數(shù)組,沒有 dataOrgCode 屬性,那這個(gè) dataOrgCode 屬性是怎么來的?
再次從頭進(jìn)行 debug 發(fā)現(xiàn)問題出現(xiàn)在 ForEachSqlNode 的 apply 方法里面
public boolean apply(DynamicContext context) { // 獲取參數(shù)映射存儲(chǔ)Map Map<String, Object> bindings = context.getBindings(); // 獲取bingdings中的parameter參數(shù),key為collectionExpression,也就是我們寫在標(biāo)簽foreach 標(biāo)簽的 collection 值里的內(nèi)容 // 根據(jù)collectionExpression從參數(shù)映射器中獲取到對(duì)應(yīng)的值, 本次的值為:["1","2"] final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings, Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach)); if (iterable == null || !iterable.iterator().hasNext()) { return true; } // 第一個(gè)參數(shù) boolean first = true; // 再拼接sql里添加我們定義在 foreach 標(biāo)簽的 open 值里的內(nèi)容 applyOpen(context); // 遍歷的計(jì)數(shù)器 int i = 0; // 遍歷我們傳進(jìn)來的數(shù)組數(shù)據(jù) ["1","2"] // o 表示我們本次遍歷數(shù)組中的值,例如 ”1“ for (Object o : iterable) { DynamicContext oldContext = context; if (first || separator == null) { context = new PrefixedContext(context, ""); } else { context = new PrefixedContext(context, separator); } int uniqueNumber = context.getUniqueNumber(); // 把 foreach 標(biāo)簽的 index 值里的內(nèi)容作為 key,計(jì)數(shù)器的值 i 作為 value 存儲(chǔ)到 bingdings 中。 // 例如第一次循環(huán)就為("index",0)。注意:由于相同的key會(huì)被覆蓋住,所以最終存儲(chǔ)的為("index",userIds.length - 1) // 同時(shí)生成一個(gè) key 為 ITEM_PREFIX + index 值內(nèi)容 + "_" + uniqueNumber,value 為 uniqueNumber 存儲(chǔ)到 bingdings 中。 // 例如第一次循環(huán)就為("__frch_index_0",0) applyIndex(context, i, uniqueNumber); // 把 foreach 標(biāo)簽的 item 值里的內(nèi)容作為 key,本次遍歷數(shù)組中的值作為 value 存儲(chǔ)到 bingdings 中。 // 例如第一次循環(huán)就為("userId","1")。注意:由于相同的key會(huì)被覆蓋住,所以最終存儲(chǔ)的為("index",userIds[userIds.length - 1]) // 同時(shí)生成一個(gè) key 為 ITEM_PREFIX + item 值內(nèi)容 + "_" + uniqueNumber,value 為本次遍歷數(shù)組中的值存儲(chǔ)到 bingdings 中。 // 例如第一次循環(huán)就為("__frch_userId_0","1") applyItem(context, o, uniqueNumber); contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); if (first) { first = !((PrefixedContext) context).isPrefixApplied(); } context = oldContext; // 計(jì)數(shù)器加一 i++; } // foreach 遍歷完,添加 foreach 標(biāo)簽定義的 close 內(nèi)容 applyClose(context); return true; }
從源碼可以知道,問題就出在遍歷 dataOrgCodes 這個(gè)數(shù)組上面。在執(zhí)行 apply 方法之中有
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
#ForEachSqlNode private void applyIndex(DynamicContext context, Object o, int i) { if (index != null) { context.bind(index, o); context.bind(itemizeItem(index, i), o); } } private void applyItem(DynamicContext context, Object o, int i) { if (item != null) { context.bind(item, o); context.bind(itemizeItem(item, i), o); } } #DynamicContext public void bind(String name, Object value) { bindings.put(name, value); }
從上面的邏輯中可以知道,在遍歷 dataOrgCodes 數(shù)組的時(shí)候,會(huì)把我們定義在 foreach 標(biāo)簽中
item、index 屬性值作為 key 存儲(chǔ)在 DynamicContext 的 bingdings 中,也就是我們傳進(jìn)來的查詢參數(shù)對(duì)象對(duì)應(yīng)的 Map 中,這就導(dǎo)致了雖然我們沒有傳進(jìn)來 dataOrgCode 屬性,但是在執(zhí)行 dataOrgCodes 的 foreach 過程中產(chǎn)生了中間值 dataOrgCode,導(dǎo)致最終拼接的 SQL 出現(xiàn)了不該有的條件語句。
按道理我們使用的框架是 Mybatis 二次開發(fā)的(基本是 Mybatis),應(yīng)該不會(huì)有這么大的問題。所以在發(fā)現(xiàn)問題后在本地寫了一個(gè) demo 進(jìn)行復(fù)現(xiàn),發(fā)現(xiàn)本地的不會(huì)出現(xiàn)這個(gè)問題,頓時(shí)疑惑了。然后就去了 github 把 Mybatis 的源碼拉下來進(jìn)行比較,最終發(fā)現(xiàn)了一些問題。
Mybatis 在 2017 年發(fā)現(xiàn)了問題并進(jìn)行了修復(fù),在方法結(jié)尾處添加了移除本次 foreach 遍歷產(chǎn)生的中間值,也就是從參數(shù)映射 Map 中刪除了我們定義在 <foreach> 標(biāo)簽的 item、index 定義的 key,這樣就不會(huì)產(chǎn)生本節(jié)的問題。
然而我所用的框架依然是沒有更新,用的還是 2012 年版本的代碼。所以為了解決這個(gè)問題,只能修改 foreach 標(biāo)簽中的 item 的屬性值名稱,避免和 if 標(biāo)簽的 test 中的屬性名稱沖突。也就是修改為以下的 SQL 代碼。
感謝各位的閱讀,以上就是“Mybatis中SQL節(jié)點(diǎn)實(shí)例分析”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)Mybatis中SQL節(jié)點(diǎn)實(shí)例分析這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。