12.動態SQL
動態 SQL 是 MyBatis 的強大特性之一。在 JDBC 或其它類似的框架中,開發人員通常需要手動拼接 SQL 語句。根據不同的條件拼接 SQL 語句是一件極其痛苦的工作。例如,拼接時要確保添加了必要的空格,還要注意去掉列表最後一個列名的逗號。而動態 SQL 恰好解決了這一問題,可以根據場景動態的構建查詢。
動態 SQL 只有幾個基本元素,與 JSTL 或 XML 文字處理器相似,十分簡單明瞭,大量的判斷都可以在 MyBatis 的對映 XML 檔案裡配置,以達到許多需要大量程式碼才能實現的功能。
動態 SQL 大大減少了編寫程式碼的工作量,更體現了 MyBatis 的靈活性、高度可配置性和可維護性。
MyBatis 也可以在註解中配置 SQL,但是由於註解功能受限,且對於複雜的 SQL 語句來說可讀性差,所以使用較少。本教程不對它們進行介紹。
MyBatis 的動態 SQL 包括以下幾種元素,如下表所示。
元素 | 作用 | 備註 |
---|---|---|
if | 判斷語句 | 單條件分支判斷 |
choose(when、otherwise) | 相當於 Java 中的 switch case 語句 | 多條件分支判斷 |
trim、where | 輔助元素 | 用於處理一些SQL拼裝問題 |
foreach | 迴圈語句 | 在in語句等列舉條件常用 |
bind | 輔助元素 | 拼接引數 |
一、if標籤:條件判斷
MyBatis if 類似於 Java 中的 if 語句,是 MyBatis 中最常用的判斷語句。使用 if 標籤可以節省許多拼接 SQL 的工作,把精力集中在 XML 的維護上。
if 語句使用方法簡單,常常與 test 屬性聯合使用。語法如下。
- <if test="判斷條件">
- SQL語句
- </if>
當判斷條件為 true 時,才會執行所包含的 SQL 語句。
最常見的場景是在 if 語句中包含 where 子句,例如。
<select id="selectAllWebsite" resultMap="myResult"> select id,name,url from website <if test="name != null"> where name like #{name} </if> </select>
以上代表表示根據網站名稱去查詢相應的網站資訊,但是網站名稱是一個可填可不填的條件,不填寫的時候不作為查詢條件。
可多個 if 語句同時使用。以下語句表示為可以按照網站名稱(name)或者網址(url)進行模糊查詢。如果您不輸入名稱或網址,則返回所有的網站記錄。但是,如果你傳遞了任意一個引數,它就會返回與給定引數相匹配的記錄。
<select id="selectAllWebsite" resultMap="myResult">
select id,name,url from website where 1=1
<if test="name != null">
AND name like #{name}
</if>
<if test="url!= null">
AND url like #{url}
</if>
</select>
二、choose、when和otherwise標籤
MyBatis 中動態語句 choose-when-otherwise 類似於 Java 中的 switch-case-default 語句。由於 MyBatis 並沒有為 if 提供對應的 else 標籤,如果想要達到<if>...<else>...</else> </if> 的效果,可以藉助 <choose>、<when>、<otherwise> 來實現。
動態語句 choose-when-otherwise 語法如下。
- <choose>
- <when test="判斷條件1">
- SQL語句1
- </when >
- <when test="判斷條件2">
- SQL語句2
- </when >
- <when test="判斷條件3">
- SQL語句3
- </when >
- <otherwise>
- SQL語句4
- </otherwise>
- </choose>
choose 標籤按順序判斷其內部 when 標籤中的判斷條件是否成立,如果有一個成立,則執行相應的 SQL 語句,choose 執行結束;如果都不成立,則執行 otherwise 中的 SQL 語句。這類似於 Java 的 switch 語句,choose 為 switch,when 為 case,otherwise 則為 default。
2.1 示例
以下示例要求:
- 當網站名稱不為空時,只用網站名稱作為條件進行模糊查詢;
- 當網站名稱為空,而網址不為空時,則用網址作為條件進行模糊查詢;
- 當網站名稱和網址都為空時,則要求網站年齡不為空。
下面使用 choose-when-otherwise 標籤實現(本節示例基於《第一個MyBatis程式》一節的程式碼實現)。
WebsiteMapper.xml 程式碼如下。
<mapper namespace="net.biancheng.mapper.WebsiteMapper">
<select id="selectWebsite"
parameterType="net.biancheng.po.Website"
resultType="net.biancheng.po.Website">
SELECT id,name,url,age,country
FROM website WHERE 1=1
<choose>
<when test="name != null and name !=''">
AND name LIKE CONCAT('%',#{name},'%')
</when>
<when test="url != null and url !=''">
AND url LIKE CONCAT('%',#{url},'%')
</when>
<otherwise>
AND age is not null
</otherwise>
</choose>
</select>
</mapper>
WebsiteMapper 類中方法如下。
- public List<Website> selectWebsite(Website website);
測試類程式碼如下。
public class Test {
public static void main(String[] args) throws IOException {
// 讀取配置檔案mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根據配置檔案構建
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
// 通過SqlSessionFactory建立SqlSession
SqlSession ss = ssf.openSession();
Website site = new Website();
site.setname("程式設計");
List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
for (Website ws : siteList) {
System.out.println(ws);
}
}
}
輸出結果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE 1=1 AND name LIKE CONCAT('%',?,'%')
DEBUG [main] - ==> Parameters: 程式設計(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=程式設計幫,url=https://www.biancheng.net/,age=10,country=CN,createtime=null]
這樣 MyBatis 就會根據引數的設定進行判斷來動態組裝 SQL,以滿足不同業務的要求。遠比 Hibernate 和 JDBC 中大量判斷 Java 程式碼要清晰和明確。
三、where標籤
SQL 語句中加入了一個條件“1=1”,如果沒有加入這個條件,那麼可能就會變成下面這樣一條錯誤的語句。
- SELECT id,name,url,age,country FROM website AND name LIKE CONCAT('%',#{name},'%')
顯然以上語句會出現 SQL 語法異常,但加入“1=1”這樣的條件又非常奇怪,所以 MyBatis 提供了 where 標籤。
where 標籤主要用來簡化 SQL 語句中的條件判斷,可以自動處理 AND/OR 條件,語法如下。
- <where>
- <if test="判斷條件">
- AND/OR ...
- </if>
- </where>
if 語句中判斷條件為 true 時,where 關鍵字才會加入到組裝的 SQL 裡面,否則就不加入。where 會檢索語句,它會將 where 後的第一個 SQL 條件語句的 AND 或者 OR 關鍵詞去掉。
3.1 示例
WebsiteMapper.xml 程式碼如下。
<select id="selectWebsite" resultType="net.biancheng.po.Website">
select id,name,url from website
<where>
<if test="name != null">
AND name like #{name}
</if>
<if test="url!= null">
AND url like #{url}
</if>
</where>
</select>
WebsiteMapper 類中方法如下。
- public List<Website> selectWebsite(Website website);
測試類程式碼如下。
public class Test {
public static void main(String[] args) throws IOException {
// 讀取配置檔案mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根據配置檔案構建
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
// 通過SqlSessionFactory建立SqlSession
SqlSession ss = ssf.openSession();
Website site = new Website();
site.setname("程式設計");
List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
for (Website ws : siteList) {
System.out.println(ws);
}
}
}
輸出結果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE name LIKE CONCAT('%',?,'%')
DEBUG [main] - ==> Parameters: 程式設計(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=程式設計幫,url=https://www.biancheng.net/,age=10,country=CN]
四、set標籤
在 Mybatis 中,update 語句可以使用 set 標籤動態更新列。set 標籤可以為 SQL 語句動態的新增 set 關鍵字,剔除追加到條件末尾多餘的逗號。
4.1示例
WebsiteMapper.xml 程式碼如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.biancheng.mapper.WebsiteMapper">
<select id="selectWebsite" resultType="net.biancheng.po.Website">
SELECT * FROM website
<where>
<if test="id!=null and id!=''">
id=#{id}
</if>
</where>
</select>
<!--使用set元素動態修改一個網站記錄 -->
<update id="updateWebsite"
parameterType="net.biancheng.po.Website">
UPDATE website
<set>
<if test="name!=null">name=#{name}</if>
<if test="url!=null">url=#{url}</if>
</set>
WHERE id=#{id}
</update>
</mapper>
WebsiteMapper 類中方法如下。
package net.biancheng.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import net.biancheng.po.Website;
public interface WebsiteMapper {
public List<Website> selectWebsite(Website site);
public int updateWebsite(Website site);
}
測試類程式碼如下。
package net.biancheng.test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import net.biancheng.mapper.WebsiteMapper;
import net.biancheng.po.Website;
public class Test {
public static void main(String[] args) throws IOException {
InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
SqlSession ss = ssf.openSession();
Website site = new Website();
site.setId(1);
site.setUrl("www.biancheng.net");
// 執行update語句前
List<Website> siteList = ss.getMapper(WebsiteMapper.class).selectWebsite(site);
for (Website st : siteList) {
System.out.println(st);
}
int num = ss.getMapper(WebsiteMapper.class).updateWebsite(site);
System.out.println("影響資料庫行數" + num);
// 執行update語句後
List<Website> siteList2 = ss.getMapper(WebsiteMapper.class).selectWebsite(site);
for (Website st : siteList2) {
System.out.println(st);
}
ss.commit();
ss.close();
}
}
輸出結果如下。
DEBUG [main] - ==> Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Website[id=1,name=程式設計幫,url=http://www.biancheng.net/,age=10,country=CN]
DEBUG [main] - ==> Preparing: UPDATE website SET url=? where id=?
DEBUG [main] - ==> Parameters: www.biancheng.net(String), 1(Integer)
DEBUG [main] - <== Updates: 1
影響資料庫行數1
DEBUG [main] - ==> Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Website[id=1,name=程式設計幫,url=www.biancheng.net,age=10,country=CN]
五、foreach標籤
對於一些 SQL 語句中含有 in 條件,需要迭代條件集合來生成的情況,可以使用 foreach 來實現 SQL 條件的迭代。
Mybatis foreach 標籤用於迴圈語句,它很好的支援了資料和 List、set 介面的集合,並對此提供遍歷的功能。語法格式如下。
- <foreach item="item" index="index" collection="list|array|map key" open="(" separator="," close=")">
- 引數值
- </foreach>
foreach 標籤主要有以下屬性,說明如下。
- item:表示集合中每一個元素進行迭代時的別名。
- index:指定一個名字,表示在迭代過程中每次迭代到的位置。
- open:表示該語句以什麼開始(既然是 in 條件語句,所以必然以
(
開始)。 - separator:表示在每次進行迭代之間以什麼符號作為分隔符(既然是 in 條件語句,所以必然以
,
作為分隔符)。 - close:表示該語句以什麼結束(既然是 in 條件語句,所以必然以
)
開始)。
使用 foreach 標籤時,最關鍵、最容易出錯的是 collection 屬性,該屬性是必選的,但在不同情況下該屬性的值是不一樣的,主要有以下 3 種情況:
- 如果傳入的是單引數且引數型別是一個 List,collection 屬性值為 list。
- 如果傳入的是單引數且引數型別是一個 array 陣列,collection 的屬性值為 array。
- 如果傳入的引數是多個,需要把它們封裝成一個 Map,當然單引數也可以封裝成 Map。Map 的 key 是引數名,collection 屬性值是傳入的 List 或 array 物件在自己封裝的 Map 中的 key。
5.1示例
現有 website 表包含以下記錄。
+----+----------------+----------------------------+-----+---------+---------------------+
| id | name | url | age | country | createtime |
+----+----------------+----------------------------+-----+---------+---------------------+
| 1 | 程式設計幫 | https://www.biancheng.net/ | 10 | CN | 2021-02-23 10:20:40 |
| 2 | C語言中文網 | http://c.biancheng.net/ | 12 | CN | 2021-03-08 11:23:27 |
| 3 | 百度 | https://www.baidu.com/ | 18 | CN | 2021-03-08 11:23:53 |
| 4 | 淘寶 | https://www.taobao.com/ | 17 | CN | 2021-03-10 10:33:54 |
| 5 | Google | https://www.google.com/ | 23 | US | 2021-03-10 10:34:34 |
| 6 | GitHub | https://github.com/ | 13 | US | 2021-03-10 10:34:34 |
| 7 | Stack Overflow | https://stackoverflow.com/ | 16 | US | 2021-03-10 10:34:34 |
| 8 | Yandex | http://www.yandex.ru/ | 11 | RU | 2021-03-10 10:34:34 |
+----+----------------+----------------------------+-----+---------+---------------------+
WebsiteMapper.xml 中程式碼如下。
<select id="selectWebsite"
parameterType="net.biancheng.po.Website"
resultType="net.biancheng.po.Website">
SELECT id,name,url,age,country
FROM website WHERE age in
<foreach item="age" index="index" collection="list" open="("
separator="," close=")">
#{age}
</foreach>
</select>
WebsiteMapper 類中相應方法如下。
- public List<Website> selectWebsite(List<Integer> ageList);
測試程式碼如下。
public class Test {
public static void main(String[] args) throws IOException {
// 讀取配置檔案mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根據配置檔案構建
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
// 通過SqlSessionFactory建立SqlSession
SqlSession ss = ssf.openSession();
List<Integer> ageList = new ArrayList<Integer>();
ageList.add(10);
ageList.add(12);
List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", ageList);
for (Website ws : siteList) {
System.out.println(ws);
}
}
}
輸出結果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE age in ( ? , ? )
DEBUG [main] - ==> Parameters: 10(Integer), 12(Integer)
DEBUG [main] - <== Total: 2
Website[id=1,name=程式設計幫,url=https://www.biancheng.net/,age=10,country=CN]
Website[id=2,name=C語言中文網,url=http://c.biancheng.net/,age=12,country=CN]
拓展
在使用 foreach 標籤時,應提前預估一下 collection 物件的長度。因為大量資料的 in 語句會影響效能,且還有一些資料庫會限制執行的 SQL 語句長度。
六、bind標籤
每個資料庫的拼接函式或連線符號都不同,例如 MySQL 的 concat 函式、Oracle 的連線符號“||”等。這樣 SQL 對映檔案就需要根據不同的資料庫提供不同的實現,顯然比較麻煩,且不利於程式碼的移植。幸運的是,MyBatis 提供了 bind 標籤來解決這一問題。
bind 標籤可以通過 OGNL 表示式自定義一個上下文變數。
比如,按照網站名稱進行模糊查詢,SQL 對映檔案如下。
<select id="selectWebsite" resultType="net.biancheng.po.Website">
<bind name="pattern" value="'%'+_parameter+'%'" />
SELECT id,name,url,age,country
FROM website
WHERE name like #{pattern}
</select>
bind 元素屬性如下。
- value:對應傳入實體類的某個欄位,可以進行字串拼接等特殊處理。
- name:給對應引數取的別名。
以上程式碼中的“_parameter”代表傳遞進來的引數,它和萬用字元連線後,賦給了 pattern,然後就可以在 select 語句中使用這個變數進行模糊查詢,不管是 MySQL 資料庫還是 Oracle 資料庫都可以使用這樣的語句,提高了可移植性。
大部分情況下需要傳遞多個引數,下面為傳遞多個引數時 bind 的用法示例。
6.1 示例
WebsiteMapper 類中方法程式碼如下。
- public List<Website> selectWebsite(Website site);
SQL 對映檔案程式碼如下。
<select id="selectWebsite" resultType="net.biancheng.po.Website">
<bind name="pattern_name" value="'%'+name+'%'" />
<bind name="pattern_url" value="'%'+url+'%'" />
SELECT id,name,url,age,country
FROM website
WHERE name like #{pattern_name}
AND url like #{pattern_url}
</select>
測試程式碼如下。
public class Test {
public static void main(String[] args) throws IOException {
// 讀取配置檔案mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根據配置檔案構建
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
// 通過SqlSessionFactory建立SqlSession
SqlSession ss = ssf.openSession();
Website site = new Website();
site.setname("程式設計");
site.setUrl("http");
List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
for (Website ws : siteList) {
System.out.println(ws);
}
}
}
執行結果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE name like ? AND url like ?
DEBUG [main] - ==> Parameters: %程式設計%(String), %http%(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=程式設計幫,url=https://www.biancheng.net/,age=10,country=CN]
七、trim標籤
在 MyBatis 中除了使用 if+where 實現多條件查詢,還有一個更為靈活的元素 trim 能夠替代之前的做法。
trim 一般用於去除 SQL 語句中多餘的 AND 關鍵字、逗號,
或者給 SQL 語句前拼接 where、set 等字尾,可用於選擇性插入、更新、刪除或者條件查詢等操作。trim 語法格式如下。
- <trim prefix="字首" suffix="字尾" prefixOverrides="忽略字首字元" suffixOverrides="忽略字尾字元">
- SQL語句
- </trim>
trim 中屬性說明如下。
屬性 | 描述 |
---|---|
prefix | 給SQL語句拼接的字首,為 trim 包含的內容加上字首 |
suffix | 給SQL語句拼接的字尾,為 trim 包含的內容加上字尾 |
prefixOverrides | 去除 SQL 語句前面的關鍵字或字元,該關鍵字或者字元由 prefixOverrides 屬性指定。 |
suffixOverrides | 去除 SQL 語句後面的關鍵字或者字元,該關鍵字或者字元由 suffixOverrides 屬性指定。 |
7.1示例
下面我們利用 trim 實現與 where 元素相同的效果。
要求:根據網站名稱或網址對網站進行模糊查詢。
WebsiteMapper.xml 程式碼如下。
<select id="selectWebsite" resultType="net.biancheng.po.Website">
SELECT id,name,url,age,country
FROM website
<trim prefix="where" prefixOverrides="and">
<if test="name != null and name !=''">
AND name LIKE CONCAT ('%',#{name},'%')
</if>
<if test="url!= null">
AND url like concat ('%',#{url},'%')
</if>
</trim>
</select>
WebsiteMapper 類中方法如下。
- public List<Website> selectWebsite(Website website);
測試類程式碼如下。
public class Test {
public static void main(String[] args) throws IOException {
// 讀取配置檔案mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根據配置檔案構建
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
// 通過SqlSessionFactory建立SqlSession
SqlSession ss = ssf.openSession();
Website site = new Website();
site.setname("程式設計");
site.setUrl("http");
List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
for (Website ws : siteList) {
System.out.println(ws);
}
}
}
輸出結果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website where name LIKE CONCAT ('%',?,'%') AND url like concat ('%',?,'%')
DEBUG [main] - ==> Parameters: 程式設計(String), http(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=程式設計幫,url=https://www.biancheng.net/,age=10,country=CN]