MyBatis-plus進階學習:邏輯刪除,自動填充,樂觀鎖插件

前言

MyBatis-plus進階內容
1.邏輯刪除
2.自動填充
3.樂觀鎖插件

本篇是本人在mp官網學習過程所產的學習筆記,主要概述了邏輯刪除,自動填充,和樂觀鎖插件的使用,希望對正在學習mp的朋友有所幫助

SQL

#創建用戶表
#創建用戶表
CREATE TABLE user (
    id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主鍵',
    name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
    age INT(11) DEFAULT NULL COMMENT '年齡',
    email VARCHAR(50) DEFAULT NULL COMMENT '郵箱',
    manager_id BIGINT(20) DEFAULT NULL COMMENT '直屬上級id',
    create_time DATETIME DEFAULT NULL COMMENT '創建時間',
	update_time DATETIME DEFAULT NULL COMMENT '修改時間',
	version INT(11) DEFAULT '1' COMMENT '版本',
	deleted INT(1) DEFAULT '0' COMMENT '邏輯刪除標識(0.未刪除,1.已刪除)',
    CONSTRAINT manager_fk FOREIGN KEY (manager_id)
        REFERENCES user (id)
)  ENGINE=INNODB CHARSET=UTF8;

#初始化數據:
INSERT INTO user (id, name, age, email, manager_id
	, create_time)
VALUES (1087982257332887553, '大boss', 40, '[email protected]', NULL
		, '2019-01-11 14:20:20'),
	(1088248166370832385, '王天風', 25, '[email protected]', 1087982257332887553
		, '2019-02-05 11:12:22'),
	(1088250446457389058, '李藝偉', 28, '[email protected]', 1088248166370832385
		, '2019-02-14 08:31:16'),
	(1094590409767661570, '張雨琪', 31, '[email protected]', 1088248166370832385
		, '2019-01-14 09:15:15'),
	(1094592041087729666, '劉紅雨', 32, '[email protected]', 1088248166370832385
		, '2019-01-14 09:48:16');

1.邏輯刪除

1.1邏輯刪除的簡介

邏輯刪除其實就是假刪除,將數據庫中代表是否被刪除的的字段狀態該爲被刪除狀態,之後你仍然能看到此條記錄

1.2在使用邏輯刪除時需要注意什麼?

配置邏輯刪除(application.properties,domain,config)

1.application.properties

如果定義的表中表示邏輯刪除字段的字段值與mp的默認邏輯刪除的值不相同時,你需要在application.properties中進行配置,相反如果你創建的表中表示邏輯刪除字段的字段值與默認邏輯刪除的相同就不需要配置

**mp的默認邏輯刪除的值:**邏輯未刪除 0 ,邏輯已刪除1

application.properties中的配置

# 配置邏輯刪除 mp的邏輯刪除默認是 0 和 1 如果與mybatis-plus的默認不相同 需要在以下配置語句中進行配置相同則不需要進行配置
# 邏輯未被刪除是0
mybatis-plus.global-config.db-config.logic-not-delete-value=0
# 邏輯已刪除爲1
mybatis-plus.global-config.db-config.logic-delete-value=1

2.domain中的表實體對象

我們可以通過@TableLogic註解對錶記錄邏輯刪除的字段值進行局部聲明(局部聲明:只是單單對於這個實體類而言,並不是全局的)

com.czxy.domain.User

package com.czxy.domain;

import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;

/**
 * @Author ScholarTang
 * @Date 2019/10/31 7:52 PM
 * @Desc 用戶類
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@TableName("user")
public class User {
    /*
    id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主鍵',
    name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
    age INT(11) DEFAULT NULL COMMENT '年齡',
    email VARCHAR(50) DEFAULT NULL COMMENT '郵箱',
    manager_id BIGINT(20) DEFAULT NULL COMMENT '直屬上級id',
    create_time DATETIME DEFAULT NULL COMMENT '創建時間',
	update_time DATETIME DEFAULT NULL COMMENT '修改時間',
	version INT(11) DEFAULT '1' COMMENT '版本',
	deleted INT(1) DEFAULT '0' COMMENT '邏輯刪除標識(0.未刪除,1.已刪除)',
     */

    //主鍵
    private Long id;
    //姓名
    private String name;
    //年齡
    private Integer age;
    //郵箱
    private String email;
    //直屬上級
    private Long managerId;
    //創建時間
    private LocalDateTime createTime;
    //修改時間
    private LocalDateTime updateTime;
    //版本
    private Integer version;
    //邏輯刪除標識(0.未刪除 1.已刪除)

    //TODO 局部配置邏輯刪除只是對應這個實體類而言,並不是全局的 delval邏輯已刪除 value邏輯未刪除
    //@TableLogic(delval = "0",value = "0")
  
  	//如果是全局配置了的,聲明此註解就行了,也可以不聲明
    @TableLogic
    private Integer deleted;
}

3.config配置類

此項配置是與各位使用的mp的版本有關,如果你使用的是3.3.1以下的那你就需要創建一個配置類在配置類中注入一個ISqlInjector對象。如果mp的版本是3.3.1或者更高就無需進行此項配置

com.czxy.config.MyBatisConfig

package com.czxy.config;

import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @Author ScholarTang
 * @Date 2019/10/31 7:56 PM
 * @Desc mybatis-plus的配置類
 */

@Component
public class MyBatisPlusConfig {


    /**
     * 在這個配置類中注入LogicSqlInjector bean對象的原因是:
     * 若mp的版本是3.3.1以下的就需要在此處注入這個bean對象
     * 若mp的版本高於3.3.1的就無需在此處注入bean對象了
     * 我使用的mp的版本是3.1.2,不需在此處注入這個bean對象
     * @return
     */
    @Bean
    private ISqlInjector sqlInjector () {
        return new LogicSqlInjector();
    }
}

1.4邏輯刪除的測試(code)

    @Autowired
    private UserMapper mapper;

    //TODO 使用邏輯刪除 刪除表中一條記錄

    /**
     * 運行測試類查看SQL語句:
     * SQL:UPDATE user SET deleted=1 WHERE id=? AND deleted=0
     * 會發現這個這條語句並沒有使用delete去刪除而是使用update去修改了者條記錄中的用來表示邏輯刪除的字段值
     */
    @Test
    void logicalDeletion() {
        int number = mapper.deleteById(1094592041087729666L);
        log.info("影響條數:"+number);
    }

    //TODO 在使用邏輯刪除了一條記錄後,查詢所有記錄看是否能查到剛剛邏輯被刪除的記錄

    /**
     * 運行測試類查看SQL語句:
     * SQL:SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user WHERE deleted=0
     * 可以很清楚的看到它查詢的所有記錄都是deleted=0的記錄(deleted=0 即爲邏輯未刪除),
     * 所以剛剛使用邏輯刪除的那條記錄並沒有被查出來,那修改當,批量修改都是這一個意思,都是對deleted=0的記錄進行操作
     */
    @Test
    void selectAll() {
        List<User> users = mapper.selectList(null);
        log.info("users:{}",users);
    }

1.5查詢中排除刪除表識字段即注意事項

SQL:SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user WHERE deleted=0

在剛剛使用邏輯刪除的操作刪除了表中的一條記錄後,查詢了所有的記錄,在查詢語句中可以看到它加了一個條件,如果說不想看到這個where條件應該怎麼實現?

實現方式:我們可以在實體類中的表示邏輯刪除狀態的屬性上添加@TableField註解在註解中使select屬性的值爲false即可實現

@TableField(select = false)
private Integer deleted;

注意事項:

1.剛剛所描述的那種方法並不能適用於自定義SQL查詢中,請看以下的列子:

我在UserMapper中自定義了一個SQL查詢方法,然後在測試類中調用了我自定義的查詢方法,查詢數據,

這種方式它並不會自動給添加限定條件,也就是說它會將邏輯已刪除的記錄也查詢出來,這時候我們可以自己去定義一下這個條件

com.czxy.mapper.UserMapper.mySelectAll

		@Select("select * from user ${ew.customSqlSegment}")
    List<User> mySelectAll (@Param(Constants.WRAPPER) Wrapper<User> wrapper);

測試

/**
	* SQL:select * from user WHERE version = ? AND deleted = ?
	*/
@Test
    void mySelectAll () {
        List<User> users = mapper.mySelectAll(
                new LambdaQueryWrapper<User>()
                        .eq(User::getVersion, 1)
                        //添加限定條件
                        .eq(User::getDeleted,0));
        log.info("users:{}",users);
    }

2.自動填充

2.1.自動填充的簡介:

再有的項目中,有新增時間,修改時間,新增人,修改人等字段。

一個比較笨的方法就是在新增時間的時候new Date(),修改時也是同樣的一個套路,這樣下來的化反覆的操作幾遍就會顯得非常的繁瑣。

在這種情況下我們可以使用mp提供的自動填充功能設置新增時間和更新時間

2.2.自動填充的實現

1.首先我們要在表對應的實體類的屬性上添加@TableField註解,根據是插入時或者更新選擇自動填充

2.在項目的目錄下創建一個填充處理器實現mp提供的原對象處理器接口,實現自動填充

3.編寫測試類,測試結果

1.@TableField註解的fill屬性的屬性值

		/**
     * 默認不處理
     */
    DEFAULT,
    /**
     * 插入時填充字段
     */
    INSERT,
    /**
     * 更新時填充字段
     */
    UPDATE,
    /**
     * 插入和更新時填充字段
     */
    INSERT_UPDATE

我要實現的是插入時和更新時自動填充時間,所以我在表對應的實體類對象中的屬性上添加

com.czxy.domain.User

		@TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    //修改時間
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;

2.在項目目錄下創建一個包,在包中編寫一個類實現mp提供的原對象處理器接口,並在類中實現插入與更新時自動填充的方法

com.czxy.component

package com.czxy.component;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * @Author ScholarTang
 * @Date 2019/10/31 11:11 PM
 * @Desc 填充處理器實現mp提供的原對象處理器接口
 */

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        //TODO 插入時自動填充時間
        /**
         * 該方法有三個參數
         * fieldName  屬性名
         * fieldVal   自動填充的值
         * metaObject 原對象參數
         */
        log.info("insert is ok");
  			//通用方法
      	//setFieldValByName() 無論是插入還是更新都可以調用
        setInsertFieldValByName("createTime",LocalDateTime.now(),metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        //TODO 更新是自動填充時間
        /**
         * 該方法有三個參數
         * fieldName  屬性名
         * fieldVal   自動填充的值
         * metaObject 原對象參數
         */
        log.info("update is ok");
      	//通用方法
      	//setFieldValByName() 無論是插入還是更新都可以調用
        setUpdateFieldValByName("updateTime",LocalDateTime.now(),metaObject);
    }
}

3.編寫測試類,測試

//TODO 自動填充測試 添加
    /**
     * 運行結果:
     * SQL:INSERT INTO user ( id, name, age, email, manager_id, create_time, version, deleted ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
     * 數據:1087982257332887778(Long), 朱茵(String), 18(Integer), [email protected](String), 1087982257332887553(Long), 2019-10-31T23:30:56.350(LocalDateTime), 1(Integer), 0(Integer)
     *
     * 結果分析:
     * look!look! 2019-10-31T23:30:56.350(LocalDateTime) 這就自動填充了
     */
    @Test
    void insertFill () {
        //創建一個User對象,並將這個User對象插入表中的記錄中
        User user = new User(1087982257332887978L,"朱茵",18,"[email protected]",1087982257332887553L,null,null,1,0);
        int rows = mapper.insert(user);
        System.out.println(rows);
        log.info("影響條數:"+rows);
    }

    //TODO 修改記錄時自動填充修改時間

    /**
     * 運行結果:
     * SQL:UPDATE user SET age=?, update_time=? WHERE id=?
     * 數據:19(Integer), 2019-10-31T23:38:52.427(LocalDateTime), 1087982257332887978(Long)
     * 
     * 結果分析:
     * 2019-10-31T23:38:52.427(LocalDateTime) 將當前時間做爲修改的時間自動填充了,修改成功,填充成功
     */
    @Test
    void updateFill () {
        //創建一個User對象,在這個對象中傳入主鍵,和要修改的字段值,並修改
        User user = new User();
        user.setId(1087982257332887978L);
        user.setAge(19);
        int rows = mapper.updateById(user);
        log.info("影響條數:"+rows);
    }

2.3.自動填充的優化

1.插入數據時自動填充的優化

比方說,在我們插入的時候對插入的時間進行自動填充,那如果我表中的字段並沒有這記錄插入時間的字段,那豈不是浪費了資源,別擔心繼續往下看有辦法解決!

解決方案:我們可以在填充處理器中進行判斷,判斷表中對應的實體對象中是否有這個屬性,如果有就填充,沒有就不填充

代碼:com.czxy.component.MyMetaObjectHandler.insertFill

 @Override
    public void insertFill(MetaObject metaObject) {

        //判斷要插入的記錄是否存在給定的字段名,返回布爾值 爲true就是有,爲false就是沒有
        //如果存在就填充,如果不存在就不填充
        //我數據庫表中是有createTime這個字段的,我改成其他的,測試結果
        boolean hasGetter = metaObject.hasGetter("createTime1");
        if (hasGetter) {
            //TODO 插入時自動填充時間
            /**
             * 該方法有三個參數
             * fieldName  屬性名
             * fieldVal   自動填充的值
             * metaObject 原對象參數
             */
            log.info("insert is ok");
            setInsertFieldValByName("createTime",LocalDateTime.now(),metaObject);
        }
    }

代碼:測試

 //TODO 測試自動填充優化 (插入)
    /**
     * 運行結果一:
     * SQL:INSERT INTO user ( id, name, age, email, manager_id, create_time, version, deleted ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
     * 數據:1087982258332887978(Long), 朱茵(String), 18(Integer), [email protected](String), 1087982257332887553(Long), null, 1(Integer), 0(Integer)
     *
     * 結果分析:
     * 從SQL語句和數據可以看出來並沒有自動填充了
     *
     * 運行結果二:(我又將填充處理器中的字段名該回我表中的字段名進行測試)
     * SQL:INSERT INTO user ( id, name, age, email, manager_id, create_time, version, deleted ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
     * 數據:1187982258332887978(Long), 朱茵(String), 18(Integer), [email protected](String), 1087982257332887553(Long), 2019-11-01T00:10:31.083(LocalDateTime), 1(Integer), 0(Integer)
     * 
     * 分析結果:
     * 2019-11-01T00:10:31.083(LocalDateTime) 顯然它自動幫我填充了時間
     *
     */
    @Test
    void autoFillOptimization01 () {
        User user = new User(1187982258332887978L,"朱茵",18,"[email protected]",1087982257332887553L,null,null,1,0);
        int rows = mapper.insert(user);
        log.info("影響條數:"+rows);
    }

2.更新數據時自動填充的優化

比如說:如果我在更新記錄的時候自己設置了更新時間,我就不想讓填充處理器幫我自動填充了,我沒有設置的時候又幫我填充,其實也是在填充處理器中對應的更新方法上稍作改動,我們只需要判斷它是否有被設置過值就OK,如果設置了就不執行自動填充,如果沒有設置就執行自動填充

代碼:com.czxy.component.MyMetaObjectHandler.updateFill

 @Override
    public void updateFill(MetaObject metaObject) {
        //這個方法就是判斷這個指定的字段是否被設置了值,如果被設置了子這個對象就不爲null,就不需要自動填充了
        //否則就是爲null,爲null時就自動填充
        Object val = getFieldValByName("updateTime", metaObject);
        if (val == null) {
            //TODO 更新是自動填充時間
            /**
             * 該方法有三個參數
             * fieldName  屬性名
             * fieldVal   自動填充的值
             * metaObject 原對象參數
             */
            log.info("update is ok");
            setUpdateFieldValByName("updateTime",LocalDateTime.now(),metaObject);
        }
    }

代碼:測試

//TODO 自動填充優化 (更新)
    /**
     * 運行結果一:(更新記錄時自己設置了更新時間)
     * SQL:UPDATE user SET age=?, update_time=? WHERE id=?
     * 數據:19(Integer), 2019-11-01T00:20:49.671(LocalDateTime), 1187982258332887978(Long)
     *
     * 結果分析:
     * 從SQL和數據是看不出來是否則是填充的,我在更新時自動填充的方法中打印了一句話,在這裏控制檯並沒有打印,所以
     * 測試結果是成功的,它沒有走填充處理器中的方法
     * 
     * 
     * 運行結果二:(更新記錄時自己沒有設置更新時間)
     * SQL:UPDATE user SET age=?, update_time=? WHERE id=? 
     * 數據:19(Integer), 2019-11-01T00:23:35.055(LocalDateTime), 1187982258332887978(Long)
     *
     * 結果分析:
     * 和運行結果一的道理一樣,控制檯有輸出我打印的語句,測試是成功的
     */
    @Test
    void autoFillOptimization02 () {
        User user = new User();
        user.setId(1187982258332887978L);
        user.setAge(19);
//        user.setUpdateTime(LocalDateTime.now());
        int rows = mapper.updateById(user);
        log.info("影響條數:"+rows);
    }

3.樂觀鎖插件

3.1.樂觀鎖的簡介

樂觀鎖的意圖:當要更新一條記錄時,希望這條記錄沒有被別人更新過,防止更新衝突。

使用樂觀鎖實現的方式一:取出數據時獲取當前的version(版本),更新時,帶上這個version,版本正確更新成功,錯誤更新失敗

爲什麼要使用樂觀鎖:

關於數據庫的鎖分爲悲觀鎖和樂觀鎖,悲觀鎖是通過數據庫的鎖機制實現的,這兩種鎖各有優缺,不是說誰好於誰。

樂觀鎖和悲觀鎖的應用場景:

適用於寫比較少的情況下,即爲多度場景。在多讀場景即使有衝突也很少發生,這樣可以省去了鎖的開銷,加大了系統整體吞吐量,如果是多寫的情況下,一般會經常產生衝突上傳的應用會不斷的重試,降低了系統的性能,所以多寫的情況下還是使用悲觀鎖。

3.2樂觀鎖的實現

3.2.1.實現步驟

1.在配置類中配置樂觀鎖插件

在實體類中找到數據庫表中表示版本號的字段名在實體對象中對應的屬性名version,並在這個屬性上添加一個@Version註解

配置類:com.czxy.config.MyBatisPlusConfig

 /**
     * 配置樂觀鎖插件
     * @return
     */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor (){
        return  new OptimisticLockerInterceptor();
    }

表實體對象User:com.czxy.domian.User

 		//版本
    @Version
    private Integer version;

測試

    //TODO 樂觀鎖實現更新(修改)記錄,改變表字段版本
/**
     * 運行結果:
     * SQL:UPDATE user SET age=?, email=?, update_time=?, version=? WHERE id=? AND version=?
     * 數據:20(Integer), [email protected](String), 2019-11-01T08:48:35.115(LocalDateTime), 2(Integer), 1187982258332887978(Long), 1(Integer)
     *
     * 結果分析:
     * 顯然是更新記錄成功了,並且它將當前的版本號等於舊的版本號加一了
     */
    @Test
    void optimisticLock01 () {
        //一般情況下都是去數據庫中查取記錄的version,但是爲了方便測試
        //我直接在這定義變量表示當前記錄在數據庫表中的version
        Integer version = 1;

        //創建一個User類,在User類對象中封裝記錄ID,以及要修改的字段,此條記錄當前的版本號
        User user = new User();
        user.setId(1187982258332887978L);
        user.setAge(20);
        user.setVersion(version);
        user.setEmail("[email protected]");

        int rows = mapper.updateById(user);
        log.info("影響條數:"+rows);
    }
3.2.2.注意事項(此處說明來自mp官方文檔)
 		@Version
    private Integer version;

特別說明:

支持的數據類型只有:int,Integer,long,Login,Date,Timestamo,LocaIDateTime

整數類型下:newVersion = oldVerSion +1

newVersion 會回寫到entity中

僅支持updateById(id) 與 update(entity,wrapper)方法

在update(entity,wrapper)方法下wrapper不能複用!

演示update(entity,wrapper)方法中wrapper被複用

//TODO 更新衝突(修改衝突)

    /**
     * 先執行的修改方法的運行結果:
     * SQL:
     * SQL: UPDATE user SET email=?, update_time=?, version=? WHERE name = ? AND age = ? AND version = ?
     * 數據:[email protected](String), 2019-11-01T09:11:35.526(LocalDateTime), 4(Integer), 王天風(String), 25(Integer), 3(Integer)
     * 結果分析:
     * 這是更新成功的,返回的影響條數爲一,並且將當前的版本好改成了舊的版本號加一
     *
     * 複用了第一個修改方法的條件構造器的修改方法運行結果:
     * SQL:UPDATE user SET email=?, update_time=?, version=? WHERE name = ? AND age = ? AND version = ? AND name = ? AND age = ? AND version = ?
     * 數據:[email protected](String), 2019-11-01T09:11:35.526(LocalDateTime), 5(Integer), 王天風(String), 25(Integer), 3(Integer), 王天風(String), 25(Integer), 4(Integer)
     * 結果分析:
     * 可以看到SQL語句中的version = ? AND name = ? AND age = ? AND version = ?,同時判斷了兩個版本號,所以這條SQL是執行不成功的
     * 返回的影響條數爲0
     *
     */
    @Test
    void optimisticLock02 () {
        //這個測試方法演示的是樂觀鎖的注意事項之在update(entity,wrapper)方法下wrapper不能複用
        //實現方式:
        //1.創建一個User對象,在User對象中傳入對應的主鍵,版本號,和要修改的字段值
        //2.創建條件構造器設置更新條件
        //3.mapper調用方法修改記錄
        //4.再創建一個User對象,在User對象中傳入對應的主鍵,版本號(這個版本號即爲上一條記錄的版本好加一)和要修改的字段值
        //5.mapper調用方法修改記錄

        User u01 = new User();
        u01.setId(1088248166370832385L);
        //方便測直接設置版本號
        u01.setVersion(3);
        u01.setEmail("[email protected]");

        QueryWrapper<User> wrapper = Wrappers.query();
        wrapper.eq("name","王天風").eq("age",25);
        int rows01 = mapper.update(u01, wrapper);
        log.info("第一個更新方法的影響條數:"+rows01);

        User u02 = new User();
        u02.setVersion(4);
        u02.setEmail("[email protected]");
        wrapper.eq("name","王天風").eq("age",25);
        int rows02 = mapper.update(u01, wrapper);
        log.info("第二個更新方法的影響條數:"+rows02);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章