1. 程式人生 > 實用技巧 >JZOJ 1038. 【SCOI2009】遊戲

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 屬性中指定的內容。

用於動態更新語句的類似解決方案叫做 setset 元素可以用於動態包含需要更新的列,忽略其它不更新的列。比如:

<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的語句。可以在一次資料庫連線中更新所有資料,避免了頻繁資料庫建立和斷開連線的開銷,可以很大程度的提高更新效率。但是這樣的問題是如果這個過程中更新出錯,將很難知道具體是哪個資料出錯。因此通常的的使用的方案是進行折中,也就是一次批量更新一部分。這樣可以分擔錯誤的概率,同時也更容易定位出錯的位置。