中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Mybatis中SQL節點實例分析

發布時間:2022-03-19 13:31:43 來源:億速云 閱讀:135 作者:iii 欄目:開發技術

這篇文章主要講解了“Mybatis中SQL節點實例分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Mybatis中SQL節點實例分析”吧!

一、文章引出原因

某天在完成項目中的一個小功能后進行自測的時候,發現存在一個很奇怪的 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,確定了自己傳進來的參數沒有什么問題,也沒有什么攔截器添加多余的參數。

三、分析 SQL 生成過程

在確定編寫 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 的。

四、分析多余 SQL 的生成

根據上面的執行步驟可以知道,我們的 bug 的產生是在

evaluator.evaluateBoolean(test, context.getBindings()) 這一步產生的。也就是在 context.getBindings() 中存在滿足 dataOrgCode != null and dataOrgCode != '' 的屬性。debug 驗證以下可知

Mybatis中SQL節點實例分析

可以看得出來,存儲參數映射的 Map 出現了 dataOrgCode 的屬性,但是我們傳遞進來的屬性只有 dataOrgCodes 數組,沒有 dataOrgCode 屬性,那這個 dataOrgCode 屬性是怎么來的?

再次從頭進行 debug 發現問題出現在 ForEachSqlNode 的 apply 方法里面

Mybatis中SQL節點實例分析

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中SQL節點實例分析

五、解決辦法

按道理我們使用的框架是 Mybatis 二次開發的(基本是 Mybatis),應該不會有這么大的問題。所以在發現問題后在本地寫了一個 demo 進行復現,發現本地的不會出現這個問題,頓時疑惑了。然后就去了 github 把 Mybatis 的源碼拉下來進行比較,最終發現了一些問題。

Mybatis中SQL節點實例分析

Mybatis中SQL節點實例分析

Mybatis 在 2017 年發現了問題并進行了修復,在方法結尾處添加了移除本次 foreach 遍歷產生的中間值,也就是從參數映射 Map 中刪除了我們定義在 <foreach> 標簽的 item、index 定義的 key,這樣就不會產生本節的問題。

然而我所用的框架依然是沒有更新,用的還是 2012 年版本的代碼。所以為了解決這個問題,只能修改 foreach 標簽中的 item 的屬性值名稱,避免和 if 標簽的 test 中的屬性名稱沖突。也就是修改為以下的 SQL 代碼。

Mybatis中SQL節點實例分析

感謝各位的閱讀,以上就是“Mybatis中SQL節點實例分析”的內容了,經過本文的學習后,相信大家對Mybatis中SQL節點實例分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

夹江县| 北辰区| 闽侯县| 安仁县| 靖西县| 青冈县| 手游| 平乐县| 仁寿县| 南汇区| 永德县| 舟山市| 旬邑县| 余江县| 兴安县| 治多县| 盖州市| 巢湖市| 安化县| 江永县| 随州市| 海原县| 论坛| 墨玉县| 莫力| 岑溪市| 云安县| 河南省| 辽阳市| 左云县| 蓝田县| 兰考县| 乌拉特后旗| 临沂市| 太仆寺旗| 五家渠市| 福贡县| 饶河县| 永登县| 潜江市| 红河县|