JZOJ 1038. 【SCOI2009】遊戲
09_2MyBatis動態SQL
MyBatis提供了一些if、choose(when、otherwise)、trim(where、set)、foreach等元素來處理動態SQL,這裡首先對這些元素進行說明介紹,接著會結合實際場景需求,來列出常見的一些操作。
用到的表資訊
-- ---------------------------- -- Table structure for person -- ---------------------------- DROP TABLE IF EXISTS `person`; CREATE TABLE `person` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `gender` varchar(255) DEFAULT NULL, `hobby` varchar(255) DEFAULT NULL, `c_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of person -- ---------------------------- BEGIN; INSERT INTO `person` VALUES (1, '曹操', 32, '1', NULL, 1); INSERT INTO `person` VALUES (2, '曹丕', 13, '1', NULL, 1); INSERT INTO `person` VALUES (3, '張飛', 28, '1', NULL, 2); INSERT INTO `person` VALUES (4, '甄宓', 27, '2', NULL, 1); INSERT INTO `person` VALUES (5, '星彩', 17, '2', NULL, 2); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
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” 的引數值需要包含查詢掩碼或萬用字元字元)。
如果希望通過 “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” 查詢的情形。若兩者都沒有傳入,就返回標記為 featured 的 BLOG(這可能是管理員認為,與其返回大量的無意義隨機 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” 示例,這次我們將 “state = ‘ACTIVE’” 設定成動態條件,看看會發生什麼。
<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 ‘someTitle’
這個查詢也會失敗。這個問題不能簡單地用條件元素來解決。這個問題是如此的難以解決,以至於解決過的人不會再想碰到這種問題。
MyBatis 有一個簡單且適合大多數場景的解決辦法。而在其他場景中,可以對其進行自定義以符合需求。而這,只需要一處簡單的改動:
<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 元素只會在子元素返回任何內容的情況下才插入 “WHERE” 子句。而且,若子句的開頭為 “AND” 或 “OR”,where 元素也會將它們去除。
如果 where 元素與你期望的不太一樣,你也可以通過自定義 trim 元素來定製 where 元素的功能。比如,和 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 關鍵字,並會刪掉額外的逗號(這些逗號是在使用條件語句給列賦值時引入的)。
來看看與 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 元素的功能非常強大,它允許你指定一個集合,宣告可以在元素體內使用的集合項(item)和索引(index)變數。它也允許你指定開頭與結尾的字串以及集合項迭代之間的分隔符。這個元素也不會錯誤地新增多餘的分隔符,看它多智慧!
提示 你可以將任何可迭代物件(如 List、Set 等)、Map 物件或者陣列物件作為集合引數傳遞給 foreach。當使用可迭代物件或者陣列時,index 是當前迭代的序號,item 的值是本次迭代獲取到的元素。當使用 Map 物件(或者 Map.Entry 物件的集合)時,index 是鍵,item 是值。
實戰場景
批量更新使用者資料
當前資料內容
select * from person
id name age gender hobby c_id
1 曹操 32 1 1
2 曹丕 13 1 1
3 張飛 28 1 2
4 甄宓 27 2 1
5 星彩 17 2 2
需求:按業務資料age和hobby欄位如果存在值則更新其內容。
update person
set
age=
case
when id=1 then 34
when id=2 then 15
else
age
end,
hobby=
case
when id=1 then '當丞相'
when id=2 then '改朝換代'
else
hobby
end
where id in(1,2,3)
<update id="batchUpdate">
update person
<set>
<trim prefix="age = case" suffix="end,">
<foreach collection="persons" item="item">
<if test="item.age !=null and item.age!=''">
when id=#{item.id} then #{item.age}
</if>
<if test="item.age ==null || item.age=''">
when id=#{item.id} then person.age
</if>
</foreach>
</trim>
<trim prefix="hobby = case" suffix="end,">
<foreach collection="persons" item="item">
<choose>
<when test="item.hobby !=null and item.hobby !=''">
when id=#{item.id} then #{item.hobby}
</when>
<otherwise>
when id=#{item.id} then person.hobby
</otherwise>
</choose>
</foreach>
</trim>
</set>
where
id in
<foreach collection="persons" item="item" open="(" close=")" separator=",">
#{item.id}
</foreach>
</update>
package com.lucky.spring.dao;
import com.lucky.spring.model.Person;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
/**
* Created by zhangdd on 2020/8/8
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class PersonMapperExtTest {
@Autowired
PersonMapperExt mapperExt;
@Test
public void batchUpdate() throws Exception {
List<Person> people = new ArrayList<>();
people.add(new Person());
if (people.size()<=0){
return;
}
mapperExt.batchUpdate(people);
}
}
需要保證集合的元素個數大於0。
這裡藉助了SQL中case ... when ... then ...的語法。拼湊成了批量更新sql的語句。可以在一次資料庫連線中更新所有資料,避免了頻繁資料庫建立和斷開連線的開銷,可以很大程度的提高更新效率。但是這樣的問題是如果這個過程中更新出錯,將很難知道具體是哪個資料出錯。因此通常的的使用的方案是進行折中,也就是一次批量更新一部分。這樣可以分擔錯誤的概率,同時也更容易定位出錯的位置。