<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
某天在完成專案中的一個小功能後進行自測的時候,發現存在一個很奇怪的 bug --- 最終執行的 SQL 與我所期望的 SQL 不一致,有一個 if 分支在我不傳特定引數的情況下被拼接在最終的 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>
②傳進來的引數
{ "dataOrgCodes":["6","2"] }
③Mybatis 列印執行的 SQL
SELECT * FROM balance WHERE data_org_code IN (?, ?) AND data_org_code = ?
列印的執行引數
{ "dataOrgCodes":["6","2"] }
學過 Mybatis 的人應該一樣就看出來了,這個 SQL 不對勁,多了一些不該有的東西。按照我們的理解,最終的執行的 SQL 應該是
SELECT * FROM balance WHERE data_org_code IN (?, ?)
但 mybatis 執行的 SQL 多了一點語句---AND data_org_code = ?
在出現這個問題後我反覆進行 debug,確定了自己傳進來的引數沒有什麼問題,也沒有什麼攔截器新增多餘的引數。
在確定編寫 XML 檔案的 if 標籤的內容以及傳進來的引數無誤後,排除了引數導致問題。那麼除了這個可能外,問題就可能出現在 SQL 的解析上,也就是 SQL 的生成那裡。那麼我們定位到 SQL 的生成地方, DynamicSqlSource#getBoundSql(我們查詢的引數物件)方法
// Configuration是Mybatis核心類,rootSqlNode 根SQL節點是我們定義在XML中的SQL語句。 //(例如<select>rootSqlNode</sselect>, 標籤中間的內容就是 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; }
可以看到方法內部顯示建立了一個 DynamicContext,這個物件就是用於儲存動態生成的 SQL。
(下面是省略了很多關於本次問題無關的程式碼,只保留有關程式碼)
public class DynamicContext { public static final String PARAMETER_OBJECT_KEY = "_parameter"; public static final String DATABASE_ID_KEY = "_databaseId"; // 儲存動態生成的SQL,類似於 StringBuilder 的角色 private final StringJoiner sqlBuilder = new StringJoiner(" "); // 唯一編號值,會在生成最終SQL和引數值對映關係的時候用到 private int uniqueNumber = 0; // 拼接SQL public void appendSql(String sql) { sqlBuilder.add(sql); } // 獲取拼接好的SQL public String getSql() { return sqlBuilder.toString().trim(); } // 獲取唯一編號,返回後進行加一 public int getUniqueNumber() { return uniqueNumber++; } }
而下一句就是解析我們編寫的 SQL,完成 SQL 的拼接
rootSqlNode.apply(context)
這裡的 rootSqlNode 是我們編寫在標籤裡的 SQL 內容,包括<if>、<foreach>、<where>標籤等內容。
rootSqlNode 物件是 SqlNode 型別。其實這裡的 SQL 語句被解析成類似於 HTML 的 DOM 節點的樹級結構,在本節的測試例子中結構類似如下(不完全正確,只做參考價值,表示 rootSqlNode 結構類似於以下結構):
<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>
這個 SqlNode 定義如下所示:
public interface SqlNode { boolean apply(DynamicContext context); }
裡面的 apply 方法是用於評估是否把這個 SqlNode 的內容拼接到最終返回的 SQL 上的,不同型別的 SqlNode 有不同的實現,例如我們本節相關的 SqlNode 型別就是為 IfSqlNode,對應這我們寫的 SQL 語句的 if 標籤,以及儲存最終的 sql 內容的 StaticTextSqlNode 型別。
public class StaticTextSqlNode implements SqlNode { // 儲存我們寫的 sql // 類似於 and data_org_code in private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) { // 呼叫 DynamicContext 物件的 sqppendSql 方法拼接最終 sql context.appendSql(text); return true; } }
public class IfSqlNode implements SqlNode { // 評估器 private final ExpressionEvaluator evaluator; // if標籤中用於判斷這個語句是否生效的 test 屬性值 // 這裡對應我們例子中的一個為 "dataOrgCodes != null and dataOrgCodes.size > 0" private final String test; // if標籤中的內容,如果if標籤中不存在其他標籤,那麼這裡的值就是StaticTextSqlNode型別的節點 // StaticTextSqlNode 節點的 text 屬性就是我們最終需要拼接的 sql 語句 private final SqlNode contents; // contents 是我們定義在 if 標籤裡面的內容, test 是 if 標籤的屬性 test 定義的內容 public IfSqlNode(SqlNode contents, String test) { this.test = test; this.contents = contents; this.evaluator = new ExpressionEvaluator(); } @Override public boolean apply(DynamicContext context) { // 使用評估器評估 if 標籤中定義的 test 中的內容是否為true if (evaluator.evaluateBoolean(test, context.getBindings())) { // 當contents為StaticTextSqlNode型別的節點時候,就把 if 標籤裡的內容拼接到 sql 上 // 否則繼續呼叫方法 apply(相當於遞迴呼叫,知道找到最下面的內容節點) contents.apply(context); return true; } return false; } }
我們可以看到這裡的
evaluator.evaluateBoolean(test, context.getBindings())
這個評估方法是通過把 test 語句內容和 我們傳進來的引數解析出來的 Map 進行比對,如果我們的引數中存在值,且值得內容符合 test 語句的判斷,則進行 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>
以及我們傳進來的引數進行比對
{ "dataOrgCodes":["6","2"] }
可以看得出來引數與 test 語句 "dataOrgCodes!= null and dataOrgCodes.size > 0" 比較是返回 true 的。
根據上面的執行步驟可以知道,我們的 bug 的產生是在
evaluator.evaluateBoolean(test, context.getBindings()) 這一步產生的。也就是在 context.getBindings() 中存在滿足 dataOrgCode != null and dataOrgCode != '' 的屬性。debug 驗證以下可知
可以看得出來,儲存引數對映的 Map 出現了 dataOrgCode 的屬性,但是我們傳遞進來的屬性只有 dataOrgCodes 陣列,沒有 dataOrgCode 屬性,那這個 dataOrgCode 屬性是怎麼來的?
再次從頭進行 debug 發現問題出現在 ForEachSqlNode 的 apply 方法裡面
public boolean apply(DynamicContext context) { // 獲取引數對映儲存Map Map<String, Object> bindings = context.getBindings(); // 獲取bingdings中的parameter引數,key為collectionExpression,也就是我們寫在標籤foreach 標籤的 collection 值裡的內容 // 根據collectionExpression從引數對映器中獲取到對應的值, 本次的值為:["1","2"] final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings, Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach)); if (iterable == null || !iterable.iterator().hasNext()) { return true; } // 第一個引數 boolean first = true; // 再拼接sql裡新增我們定義在 foreach 標籤的 open 值裡的內容 applyOpen(context); // 遍歷的計數器 int i = 0; // 遍歷我們傳進來的陣列資料 ["1","2"] // o 表示我們本次遍歷陣列中的值,例如 」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 標籤的 index 值裡的內容作為 key,計數器的值 i 作為 value 儲存到 bingdings 中。 // 例如第一次迴圈就為("index",0)。注意:由於相同的key會被覆蓋住,所以最終儲存的為("index",userIds.length - 1) // 同時生成一個 key 為 ITEM_PREFIX + index 值內容 + "_" + uniqueNumber,value 為 uniqueNumber 儲存到 bingdings 中。 // 例如第一次迴圈就為("__frch_index_0",0) applyIndex(context, i, uniqueNumber); // 把 foreach 標籤的 item 值裡的內容作為 key,本次遍歷陣列中的值作為 value 儲存到 bingdings 中。 // 例如第一次迴圈就為("userId","1")。注意:由於相同的key會被覆蓋住,所以最終儲存的為("index",userIds[userIds.length - 1]) // 同時生成一個 key 為 ITEM_PREFIX + item 值內容 + "_" + uniqueNumber,value 為本次遍歷陣列中的值儲存到 bingdings 中。 // 例如第一次迴圈就為("__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; // 計數器加一 i++; } // foreach 遍歷完,新增 foreach 標籤定義的 close 內容 applyClose(context); return true; }
從原始碼可以知道,問題就出在遍歷 dataOrgCodes 這個陣列上面。在執行 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 陣列的時候,會把我們定義在 foreach 標籤中
item、index 屬性值作為 key 儲存在 DynamicContext 的 bingdings 中,也就是我們傳進來的查詢引數物件對應的 Map 中,這就導致了雖然我們沒有傳進來 dataOrgCode 屬性,但是在執行 dataOrgCodes 的 foreach 過程中產生了中間值 dataOrgCode,導致最終拼接的 SQL 出現了不該有的條件語句。
按道理我們使用的框架是 Mybatis 二次開發的(基本是 Mybatis),應該不會有這麼大的問題。所以在發現問題後在本地寫了一個 demo 進行復現,發現原生的不會出現這個問題,頓時疑惑了。然後就去了 github 把 Mybatis 的原始碼拉下來進行比較,最終發現了一些問題。
Mybatis 在 2017 年發現了問題並進行了修復,在方法結尾處新增了移除本次 foreach 遍歷產生的中間值,也就是從引數對映 Map 中刪除了我們定義在 <foreach> 標籤的 item、index 定義的 key,這樣就不會產生本節的問題。
然而我所用的框架依然是沒有更新,用的還是 2012 年版本的程式碼。所以為了解決這個問題,只能修改 foreach 標籤中的 item 的屬性值名稱,避免和 if 標籤的 test 中的屬性名稱衝突。也就是修改為以下的 SQL 程式碼。
使用二次開發的框架可能存在坑,需要注意參照的版本存在未解決問題。
到此這篇關於Mybatis中SQL節點的文章就介紹到這了,更多相關Mybatis中SQL節點解析內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45