深入 MyBatis 的秘密 動(dòng)態(tài)SQL

需要了解更多可以參考這篇文章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的核心部分。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容