MyBatis動態SQL語句


MyBatis 的強大特性之一便是它的動態 SQL。如果你有使用 JDBC 或其他類似框架的經驗,你就能體會到根據不同條件拼接 SQL 語句有多麼痛苦。拼接的時候要確保不能忘了必要的空格,還要注意省掉列名列表最後的逗號。利用動態 SQL 這一特性可以徹底擺脫這種痛苦。

通常使用動態 SQL 不可能是獨立的一部分,MyBatis 當然使用一種強大的動態 SQL 語言來改進這種情形,這種語言可以被用在任意的 SQL 對映語句中。

動態 SQL 元素和使用 JSTL 或其他類似基於 XML 的文字處理器相似。在 MyBatis 之前的版本中,有很多的元素需要來了解。MyBatis 3 大大提升了它們,現在用不到原先一半的元素就可以了。MyBatis 採用功能強大的基於 OGNL 的表示式來消除其他元素。

mybatis 的動態sql語句是基於OGNL表示式的。可以方便的在 sql 語句中實現某些邏輯. 總體說來mybatis 動態SQL 語句主要有以下幾類:
1. if 語句 (簡單的條件判斷)
2. choose (when,otherwize) ,相當於java 語言中的 switch ,與 jstl 中的choose 很類似.
3. trim (對包含的內容加上 prefix,或者 suffix 等,字首,字尾)
4. where (主要是用來簡化sql語句中where條件判斷的,能智慧的處理 and or ,不必擔心多餘導致語法錯誤)
5. set (主要用於更新時)
6. foreach (在實現 mybatis in 語句查詢時特別有用)

if

動態 SQL 通常要做的事情是有條件地包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG 
  WHERE state = ‘ACTIVE’ 
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

這條語句提供了一個可選的文字查詢型別的功能。如果沒有傳入「title」,那麼所有處於「ACTIVE」狀態的BLOG都會返回;反之若傳入了「title」,那麼就會把模糊查詢「title」內容的BLOG結果返回(就這個例子而言,細心的讀者會發現其中的引數值是可以包含一些掩碼或萬用字元的)。

如果想可選地通過「title」和「author」兩個條件搜尋該怎麼辦呢?首先,改變語句的名稱讓它更具實際意義;然後只要加入另一個條件即可。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’ 
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose, when, otherwise

有些時候,我們不想用到所有的條件語句,而只想從中擇其一二。針對這種情況,MyBatis 提供了 choose 元素,它有點像 Java 中的 switch 語句。

還是上面的例子,但是這次變為提供了「title」就按「title」查詢,提供了「author」就按「author」查詢,若兩者都沒有提供,就返回所有符合條件的BLOG(實際情況可能是由管理員按一定策略選出BLOG列表,而不是返回大量無意義的隨機結果)。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim, where, set

前面幾個例子已經合宜地解決了一個臭名昭著的動態 SQL 問題。現在考慮回到「if」範例,這次我們將「ACTIVE = 1」也設定成動態的條件,看看會發生什麼。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG 
  WHERE 
  <if test="state != null">
    state = #{state}
  </if> 
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果這些條件沒有一個能匹配上將會怎樣?最終這條 SQL 會變成這樣:

SELECT * FROM BLOG
WHERE

這會導致查詢失敗。如果僅僅第二個條件匹配又會怎樣?這條 SQL 最終會是這樣:

SELECT * FROM BLOG
WHERE 
AND title like tw511.com

這個查詢也會失敗。這個問題不能簡單的用條件句式來解決,如果你也曾經被迫這樣寫過,那麼你很可能從此以後都不想再這樣去寫了。

MyBatis 有一個簡單的處理,這在90%的情況下都會有用。而在不能使用的地方,你可以自定義處理方式來令其正常工作。一處簡單的修改就能得到想要的效果:


<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG 
  <where> 
    <if test="state != null">
         state = #{state}
    </if> 
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素知道只有在一個以上的if條件有值的情況下才去插入「WHERE」子句。而且,若最後的內容是「AND」或「OR」開頭的,where 元素也知道如何將他們去除。

如果 where 元素沒有按正常套路出牌,我們還是可以通過自定義 trim 元素來客製化我們想要的功能。比如,和 where 元素等價的自定義 trim 元素為:

<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>

prefixOverrides 屬性會忽略通過管道分隔的文字序列(注意此例中的空格也是必要的)。它帶來的結果就是所有在 prefixOverrides 屬性中指定的內容將被移除,並且插入 prefix 屬性中指定的內容。

類似的用於動態更新語句的解決方案叫做 set。set 元素可以被用於動態包含需要更新的列,而捨去其他的。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

這裡,set 元素會動態前置 SET 關鍵字,同時也會消除無關的逗號,因為用了條件語句之後很可能就會在生成的賦值語句的後面留下這些逗號。

若你對等價的自定義 trim 元素的樣子感興趣,那這就應該是它的真面目:

<trim prefix="SET" suffixOverrides=","> ... </trim>

注意這裡我們忽略的是字尾中的值,而又一次附加了字首中的值。

foreach

動態 SQL 的另外一個常用的必要操作是需要對一個集合進行遍歷,通常是在構建 IN 條件語句的時候。比如:

<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>

foreach 元素的功能是非常強大的,它允許你指定一個集合,宣告可以用在元素體內的集合項和索引變數。它也允許你指定開閉匹配的字串以及在疊代中間放置分隔符。這個元素是很智慧的,因此它不會偶然地附加多餘的分隔符。

注意 你可以將一個 List 範例或者陣列作為引數物件傳給 MyBatis,當你這麼做的時候,MyBatis 會自動將它包裝在一個 Map 中並以名稱為鍵。List 範例將會以「list」作為鍵,而陣列範例的鍵將是「array」。

到此我們已經完成了涉及 XML 組態檔案和 XML 對映檔案的討論。下一部分將詳細探討 Java API,這樣才能從已建立的對映中獲取最大利益。

bind

bind 元素可以從 OGNL 表示式中建立一個變數並將其系結到上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

Multi-db vendor support

一個組態了「_databaseId」變數的 databaseIdProvider 對於動態程式碼來說是可用的,這樣就可以根據不同的資料庫廠商構建特定的語句。比如下面的例子:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

動態 SQL 中可插拔的指令碼語言

MyBatis 從 3.2 開始支援可插拔的指令碼語言,因此你可以在插入一種語言的驅動(language driver)之後來寫基於這種語言的動態 SQL 查詢。

可以通過實現下面介面的方式來插入一種語言:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

一旦有了自定義的語言驅動,你就可以在 mybatis-config.xml 檔案中將它設定為預設語言:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

除了設定預設語言,你也可以針對特殊的語句指定特定語言,這可以通過如下的 lang 屬性來完成:

<select id="selectBlog" lang="myLanguage"> SELECT * FROM BLOG </select>

或者在你正在使用的對映中加上註解 @Lang 來完成:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}