MyBatisPlus解決邏輯刪除與唯一索引的相容問題

2023-04-14 06:01:11

需求背景

比如有張使用者表,在插入或者更新資料的時候,我們需要 使用者名稱稱(username),不能重複。

我們首先考慮的是給該欄位建立唯一索引

create unique index uni_username on user(username)

似乎這樣就可以了,然而事情並沒有那麼簡單。

因為我們表中的資料在刪除的時候不會真的的刪除,而是採用邏輯刪除,會有一個 deleted 欄位使用0,1標識未刪除與已刪除。

當然我們可以考慮將 username + deleted 組合成一個聯合唯一索引。

create unique index uni_username_deleted on user(username,deleted)

這樣就ok了嗎?

其實會有一個新的問題,就是如果同一個使用者名稱如果被刪除一次。

再去刪除會發現系統報錯了,因為該條資料已經存在了,不能在刪除了。

是不是很多時候因為邏輯刪除與唯一索引的衝突,你就不建立唯一索引,想著自己寫的程式碼自己有信心不會出現髒資料的。

這麼想你就太天真啦,資料庫是我們最後一道防線,這道防線都不要了嘛?

阿里巴巴手冊有關索引規範,第一條就是

【強制】業務上具有唯一特性的欄位,即使是組合欄位,也必須建成唯一索引。

手冊還有這麼一句話:

即使在應用層做了非常完善的校驗和控制,只要沒有唯一索引,根據墨菲定律,必然有髒資料產生。

所以唯一索引非常有必要!!!

那該怎麼做能讓邏輯刪除與唯一索引相容?

現在大家比較通用的辦法就是

我們依舊可以將 username + deleted 組合成一個聯合唯一索引,但是刪除的時候deleted不再是固定的1,而是當前的主鍵ID,也就是deleted不等於0都是刪除狀態,如果刪除了那deleted值=id值

既然確立瞭解決方案,那就該思考怎麼做?


二、MyBatisPlus邏輯刪除

MyBatisPlus是支援邏輯刪除的,如果確定在哪個欄位是邏輯刪除欄位,那就在該欄位上新增一個註解

  /**
     * 1、刪除 0、未刪除
     */
    @TableLogic(value = "0", delval = "1")
    private Integer deleted;

這個一來運算元據是會自動變成如下:

  • 查詢時: 查詢條件會自動加上 'AND deleted = 0'
  • 刪除時: 自定新增 'UPDATE SET deleted = 1 … WHERE … AND deleted = 0'

如果你想刪除的時候不再是固定1而是id值,那麼就可以這樣改

    @TableLogic(value = "0", delval = "id")
    private Integer deleted;

如果想改成全域性的那麼在組態檔中新增

mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 1 # 邏輯已刪除值(預設為 1)
      logic-not-delete-value: 0 # 邏輯未刪除值(預設為 0)

三、測試

1、使用者表

CREATE TABLE `user` (
  `id` int unsigned  AUTO_INCREMENT COMMENT '主鍵',
  `username` varchar(128)  COMMENT '使用者名稱',
  `phone` varchar(32)  COMMENT '手機號',
  `sex` char(1)  COMMENT '性別',
  `create_time` datetime  COMMENT '建立時間',
  `update_time` datetime  COMMENT '更新時間',
  `deleted` tinyint DEFAULT '0' COMMENT '1、刪除 0、未刪除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 

2、建立對應實體

@Data
@Accessors(chain = true)
@TableName("user")
public class UserDO implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Integer id;
    /**
     * 使用者名稱
     */
    private String username;
    /**
     * 手機號
     */
    private String phone;
    /**
     * 性別
     */
    private String sex;
    /**
     * 建立時間
     */
    private LocalDateTime createTime;
    /**
     * 更新時間
     */
    private LocalDateTime updateTime;

    /**
     * 1、刪除 0、未刪除
     */
    private Integer deleted;
}

3、物理刪除測試

注意: 目前 deleted 欄位是沒有新增 @TableLogic註解,同是在全域性也沒有定義邏輯刪除

我們來看下刪除範例

    @Test
    public void deleteById() {
        //方式一:根據id刪除
        mapper.deleteById(10);
        //方式二:根據指定欄位刪除
        LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserDO::getSex, "男");
        mapper.delete(wrapper);
        //方式三:手動邏輯刪除
        UserDO userDO = new UserDO();
        userDO.setId(10);
        userDO.setDeleted(1);
        mapper.updateById(userDO);
    }

執行結果

--方式1
DELETE FROM user WHERE id=10
--方式2
DELETE FROM user WHERE (sex = '男')
--方式3
UPDATE user SET deleted=1 WHERE id=10

我們通過結果可以看出,如果不新增邏輯刪除標識 那刪除就是物理刪除。

4、邏輯刪除測試

我們在deleted屬性欄位 新增 邏輯刪除標識

 @TableLogic(value = "0", delval = "id")
 private Integer deleted;

我們再來執行上面三個刪除,看下執行結果

--方式1
UPDATE user SET deleted=id WHERE id=10 AND deleted=0
--方式2
UPDATE user SET deleted=id WHERE deleted=0 AND (sex = '男')
--方式3
報錯了

從執行結果來看,方式一和方式二都從之前的物理刪除變成了邏輯刪除。

但為什麼方式三會報錯呢?我們來看下報錯的結果

發現問題了,最終執行的SQL竟然是:

UPDATE user  WHERE id=?  AND deleted=0

為什麼是這樣,正常不應該是

UPDATE user SET deleted=1  WHERE id=?  AND deleted=0

這個就需要去看Mybatisplus到底做了什麼操作,改變了我們的SQL

真相大白了

Mybatisplus在updateById更新時,如果已經加了邏輯刪除標記,那做SQL拼接的時候,會自動過濾掉邏輯刪除的Set拼接

所以在實際開發中就非常注意,如果你的專案一開始是沒有加Mybatisplus邏輯刪除標識的,後面你在加邏輯刪除標識時,不是說加了就好了。

你還需要考慮對整體專案有沒有影響,如果之前是用updateById做邏輯刪除,那就會導致之前的刪除失敗甚至是報錯,這一點一定要注意。

本人有踩過坑!



宣告: 公眾號如需轉載該篇文章,發表文章的頭部一定要 告知是轉至公眾號: 後端元宇宙。同時也可以問本人要markdown原稿和原圖片。其它情況一律禁止轉載!