MyBatis-Plus——實踐篇

卡斯特梅的雨傘發表於2021-10-08

MyBatis-Plus——實踐篇

MyBatis-Plus (簡稱 MP)是一個 MyBatis的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。進行資料庫操作常用的第三方元件。

目前主流的持久層框架:

JPA (springboot用得多)、 tk-mapper(通用框架用的多)、MyBatisPlus

一句話概括:MyBatis 本來就是簡化 JDBC 操作的,而MyBatis-Plus用於簡化 MyBatis 操作。

特性

  • 無侵入:只做增強不做改變,引入它不會對現有工程產生影響,如絲般順滑
  • 損耗小:啟動即會自動注入基本 CURD,效能基本無損耗,直接物件導向操作。BaseMapper<物件泛型>進行操作。
  • 強大的 CRUD 操作:內建通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作(簡單的CRUD操作不需要再編寫xml語句),更有強大的條件構造器,滿足各類使用需求
  • 支援 Lambda 形式呼叫:通過 Lambda 表示式,方便的編寫各類查詢條件,無需再擔心欄位寫錯
  • 支援主鍵自動生成:支援多達 4 種主鍵策略(內含分散式唯一 ID 生成器 - Sequence,雪花演算法實現),可自由配置,完美解決主鍵問題
  • 支援 ActiveRecord 模式:支援 ActiveRecord 形式呼叫,實體類只需繼承 Model 類即可進行強大的 CRUD 操作
  • 支援自定義全域性通用操作:支援全域性通用方法注入( Write once, use anywhere )
  • 內建程式碼生成器:採用程式碼或者 Maven 外掛可快速生成 Mapper 、 Model 、 Service 、 Controller 層程式碼,支援模板引擎,更有超多自定義配置等您來使用
  • 內建分頁外掛基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好外掛之後,寫分頁等同於普通 List 查詢
  • 分頁外掛支援多種資料庫:支援 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多種資料庫
  • 內建效能分析外掛:可輸出 Sql 語句以及其執行時間,建議開發測試時啟用該功能,能快速揪出慢查詢
  • 內建全域性攔截外掛:提供全表 delete 、 update 操作智慧分析阻斷,也可自定義攔截規則,預防誤操作

支援資料庫

任何能使用 mybatis 進行 crud, 並且支援標準 sql 的資料庫

框架結構

image.png

快速入門

使用第三方元件一般通用步驟:前三步是基礎入門,最後一步是使用元件的擴充套件技術。
1、匯入對應的依賴
2、研究依賴如何配置
3、程式碼如何編寫
4、提高擴充套件技術能力

專案準備:

1、資料庫建立:
image.png

2、表和資料:

DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主鍵ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年齡',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '郵箱',
version int(5) NULL DEFAULT NULL COMMENT '版本號',
gmt_create datetime NULL DEFAULT NULL COMMENT '建立時間',
gmt_modified datetime NULL DEFAULT NULL COMMENT '修改時間',
delete_flag int(1) NULL DEFAULT NULL COMMENT '是否刪除 0:否,1:是',
PRIMARY KEY (id)
);


INSERT INTO user (id, name, age, email,version,gmt_create,gmt_modified,delete_flag) VALUES
(1, 'Jone', 18, 'test1@baomidou.com',1,now(),null,0),
(2, 'Jack', 20, 'test2@baomidou.com',1,now(),null,0),
(3, 'Tom', 28, 'test3@baomidou.com',1,now(),null,0),
(4, 'Sandy', 21, 'test4@baomidou.com',1,now(),null,0),
(5, 'Billie', 24, 'test5@baomidou.com',1,now(),null,0);

3、idea外掛:lombok,簡化bean物件的getter/setter、構造器等的編寫。

4、建立springboot專案

5、匯入依賴

	<dependencies>
<!--		mysql資料庫驅動-->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
<!--		lombok依賴-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
<!--		mybatis-plus依賴-->
	<!--	使用 mybatis-plus 可以節省我們大量的程式碼,因為!版本的差異,儘量不要同時匯入 mybatis 和 mybatisplus-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.0.5</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
	</dependencies>

6、連線mysql資料庫

# mysql 5 驅動不同 com.mysql.jdbc.Driver
#useSSL=true 表示是否使用安全連線,mac如果報錯就改為false
#useUnicode=true&characterEncoding=utf-8 表示是否使用字符集編碼並給定字符集編碼型別utf-8
#serverTimezone=GMT%2B8 時區的配置,mysql8使用
spring:
    datasource:
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
# mysql 8 驅動不同com.mysql.cj.jdbc.Driver(雖然驅動不同,但是這個驅動是向上相容mysql5的)、需要增加時區的配置,serverTimezone=GMT%2B8
#spring:
#    datasource:
#        username: root
#        password: 123456
#        driver-class-name: com.mysql.cj.jdbc.Driver
#        url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8

7、使用mybatis-plus進行單元測試

示例程式碼:

pojo:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Integer version;
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
    private Integer deleteFlag;
}

dao或者mapper介面:

//可以使用@Mapper或@Repository代表持久層,注入spring容器
//@Mapper
@Repository
public interface UserDao extends BaseMapper<User> {
// 在對應的Mapper上面繼承基本的類 BaseMapper,所有的CRUD操作都已經編寫完成了,直接使用BaseMapper上定義的方法
}

啟動類上去掃描我們的mapper包下的所有介面:@MapperScan("com.kuang.mapper")

//啟動類上去掃描我們的dao包下的所有介面
@MapperScan("com.wayne.dao")
@SpringBootApplication
public class MybatisPlusApplication {
	public static void main(String[] args) {
		SpringApplication.run(MybatisPlusApplication.class, args);
	}
}

測試類:

@SpringBootTest
class MybatisPlusApplicationTests {
	// 繼承了BaseMapper,所有的方法都來自己父類
// 我們也可以編寫自己的擴充套件方法!
	@Autowired
	private UserDao userDao;

	@Test
	public void testQuery(){
		//引數是一個 Wrapper,條件構造器,這裡我們先不用 null,預設查詢全部使用者
		List<User> users = userDao.selectList(null);
		users.forEach(System.out::println);
	}
}

結果:

User(id=1, name=Jone, age=18, email=test1@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=2, name=Jack, age=20, email=test2@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=3, name=Tom, age=28, email=test3@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=5, name=Billie, age=24, email=test5@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)

報錯:The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone.

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDao' defined in file [F:\waynecode\mybatis_plus\target\classes\com\wayne\dao\UserDao.class]: Unsatisfied dependency expressed through bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.ibatis.session.SqlSessionFactory]: Factory method 'sqlSessionFactory' threw exception; nested exception is com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Error: GlobalConfigUtils setMetaData Fail !  Cause:java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the 'serverTimezone' configuration property) to use a more specific time zone value if you want to utilize time zone support.

A:這是因為我們的mysql-connector-java依賴預設的版本是mysql8的,需要配置時區。在mysql連線的url配置上增加時區配置serverTimezone=GMT%2B8。GMT%2B8表示東8區。還有需要注意的是serverTimezone不要寫錯成serverTimeZone。

Q:@Repository是用於修飾類,讓spring掃描註冊到容器中,那麼介面上加@Repository有意義麼?

A: 在介面上加@Repository註解沒有意義

首先,如果AttachmentFileMapper不加@Repository註解的話有些版本的idea會報NoSuchBeanDefinitionException的紅色下劃線錯誤,實際是可以啟動成功的,並沒有什麼影響。其次,@Repository註解作用是告訴spring容器這是個bean例項,需要掃描載入到容器中,但是我們用@Repository標記的AttachmentFileMapper是個介面,是無法建立bean例項的。所以在介面上加@Repository註解沒有意義;最後,AttachmentFileMapper之所以只有介面而沒有實現,在執行時也沒有報錯的原因是mybatis通過@MapperScan 掃描這些mapper介面重新註冊beandefinition,對 BeanDefinition 進行了加工 processBeanDefinitions() 。當我們需要AttachmentFileMapper時會去工廠( MapperFactoryBean )裡面扔了個 AttachmentFileMapper.class 的引數進去,工廠的 getObject() 方法給我們返回了它製造的 AttachmentFileMapper。

日誌配置

用於開發日誌列印,需要擴充套件slf4j介面,logback實現的日誌配置。

#日誌配置
mybatis-plus:
    configuration:
        #預設控制檯日誌輸出配置StdOutImpl,如果要使用slf4j,logback,log4j2輸出需要加對應元件依賴和更改log-impl實現的日誌類
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

CRUD實踐

插入操作

示例:

@Test
public void testInsert(){
    User user = new User();
    user.setName("大青山");
    user.setAge(20);
    user.setEmail("daqingshan@qq.com");
    int result = userDao.insert(user);//幫我們自動生成id,預設主鍵生成策略是ID_WORKER 全域性唯一id(雪花演算法),id=1339951110603628546
    System.out.println(result);//1 ,受影響的行數
    System.out.println(user);//// 注意:id會自動回填到user物件中
}

輸出:

1
User(id=1339951110603628546, name=大青山, age=20, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)

主鍵生成策略

資料庫插入的id的預設值為:全域性的唯一id

預設 ID_WORKER 全域性唯一id(即雪花演算法)

分散式系統唯一ID生成方案彙總

雪花演算法:
snowflake是Twitter開源的分散式ID生成演算法,結果是一個long型的ID。其核心思想是:使用41bit作為
毫秒數,10bit作為機器的ID(5個bit是資料中心,5個bit的機器ID),12bit作為毫秒內的流水號(意味
著每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。可以保證幾乎全球唯
一!

主鍵自增

1、實體類欄位上 @TableId(type = IdType.AUTO)
2、資料庫欄位一定要是自增,AUTO_INCREMENT

報錯:主鍵自增時報 Field 'id' doesn't have a default value。

### Cause: java.sql.SQLException: Field 'id' doesn't have a default value
; Field 'id' doesn't have a default value; nested exception is java.sql.SQLException: Field 'id' doesn't have a default value

A:這是因為當我們設定id為主鍵自增時,即@TableId(type = IdType.AUTO),資料庫表相應的也要勾選儲存主鍵為自動遞增,即AUTO_INCREMENT。

其他自增策略

public enum IdType {
    AUTO(0), // 資料庫id自增
    NONE(1), // 未設定主鍵
    INPUT(2), // 手動輸入
    ID_WORKER(3), // 預設的全域性唯一id
    UUID(4), // 全域性唯一id uuid
    ID_WORKER_STR(5); //ID_WORKER 字串表示法
}

更新操作

所有的sql都是自動幫你動態配置的。

示例:

 @Test
    public void testUpdate() {
        //// 通過條件自動拼接動態sql
        User user = new User();
        user.setName("大青山GREEN");
        user.setAge(210);
        user.setEmail("daqingshan@qq.com");
        //UPDATE user SET name=?, age=?, email=? WHERE id=?  注意如果id為null則執行成功但沒有行資料被更新,result=0
        user.setId(9L);
        // 注意:updateById 但是引數是一個 物件!
        int result = userDao.updateById(user);
        System.out.println(result);//1 ,受影響的行數
        System.out.println(user);//注意更新後的值也會回填到user物件中 User(id=9, name=大青山GREEN, age=210, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)
    }

自動填充

建立時間、修改時間!這些個操作一遍都是自動化完成的,我們不希望手動更新!
阿里巴巴開發手冊:所有的資料庫表:gmt_create、gmt_modified幾乎所有的表都要配置上!而且需
要自動化!

方式一:資料庫級別

資料庫加入預設值,由資料庫完成自動填充。

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `name` varchar(30) NOT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年齡',
  `email` varchar(50) DEFAULT NULL COMMENT '郵箱',
  `version` int(5) DEFAULT NULL COMMENT '版本號',
  `gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
  `delete_flag` int(1) DEFAULT NULL COMMENT '是否刪除 0:否,1:是',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

方式二:程式碼級別

這時候就不需要資料庫表設計欄位預設值了。

1、實體類欄位屬性上需要增加註解

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime gmtModified;

2、編寫處理器來處理這個註解,實現MetaObjectHandler介面。有兩種寫法setFieldValByNamestrictInsertFill

3.0.5版本寫法:

//把處理器加到IOC容器中!
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    //// 插入時的填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        log.error("insertFill start...........");
        this.setFieldValByName("gmtCreate", LocalDateTime.now(),metaObject);
        this.setFieldValByName("gmtModified", LocalDateTime.now(),metaObject);
        log.error("insertFill end...........");
    }

    // 更新時的填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        log.error("updateFill start...........");
        this.setFieldValByName("gmtModified", LocalDateTime.now(),metaObject);
        log.error("updateFill end...........");
    }
}

3.3.0之後版本3.4.2,當然前面的版本也相容:

@Slf4j
@Component
public class MyNewMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.error("insertFill start...........");
        this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推薦使用)
        // 或者
        this.strictUpdateFill(metaObject, "gmtModified", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推薦)

        log.error("insertFill end...........");
 }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.error("insertFill start...........");
        this.strictUpdateFill(metaObject, "gmtModified", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推薦)

        log.error("insertFill end...........");
    }
}

樂觀鎖

樂觀鎖 : 故名思意十分樂觀,它總是認為不會出現問題,無論幹什麼不去上鎖!如果出現了問題,
再次更新值測試
悲觀鎖:故名思意十分悲觀,它總是認為總是出現問題,無論幹什麼都會上鎖!再去操作!

樂觀鎖實現方式:
取出記錄時,獲取當前 version
更新時,帶上這個version
執行更新時, set version = newVersion where version = oldVersion
如果version不對,就更新失敗

--樂觀鎖:1、先查詢,獲得版本號 version = 1
update user set name = "kuangshen", version = version + 1
where id = 2 and version = 1

測試Mybatis-plus的樂觀鎖外掛實現

1、資料庫表加version欄位。

  `version` int(5) DEFAULT '1' COMMENT '版本號',

2、實體類加對應的欄位

//樂觀鎖Version註解
@Version
private Integer version;

3、註冊元件

//啟動類上去掃描我們的dao包下的所有介面,我們可以把@MapperScan放在我們的MP配置類上也行
@MapperScan("com.wayne.dao")
@Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig {

    // 註冊樂觀鎖外掛
    @Bean
    public OptimisticLockerInterceptor getOptimisticLockerInterceptor (){
        return new OptimisticLockerInterceptor();
    }

// 註冊樂觀鎖外掛
/*    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mbp = new MybatisPlusInterceptor();
        mbp.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mbp;
    }*/
}

4、測試:

   @Test
    public void testOptimistic() {
        User user = userDao.selectById(9L);
        user.setName("大青山GGFG");
        user.setAge(21);
        user.setEmail("daqingshan@qq.com");
        int result = userDao.updateById(user);
        System.out.println(result);//1 ,受影響的行數
        System.out.println(user);//注意更新後的值也會回填到user物件中 User(id=9, name=大青山GREEN, age=210, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)
    }

    @Test
    public void testOptimisticFail() {
        User user = userDao.selectById(9L);
        user.setName("大青山GGFG");
        user.setAge(21);
        user.setEmail("daqingshan@qq.com");
        // 模擬另外一個執行緒執行了插隊操作
        User user2 = userDao.selectById(9L);
        user2.setName("大青山wwwwwww");
        user2.setEmail("daqingshan@163.com");
        userDao.updateById(user2);
        //注意這裡並不會報錯,而是會返回執行影響行數為0,表示沒有update成功。因為下面這個語句的version值改變了所以更新失敗
        //PDATE user SET name=?, age=?, email=?, version=?, gmt_modified=? WHERE id=? AND version=?
        int result = userDao.updateById(user);
        System.out.println(result);//1 ,受影響的行數
        //自旋鎖來多次嘗試提交!重新查一遍才能得到最新的version
    /*    while (0 == userDao.updateById(user)) {
            user = userDao.selectById(9L);
            user.setName("大青山GGFG");
            user.setAge(21);
            user.setEmail("daqingshan@qq.com");
        }*/
        System.out.println(user);//注意更新後的值也會回填到user物件中 User(id=9, name=大青山GREEN, age=210, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)
    }

//待補充

疑問:

1、在引入maven依賴的時候為什麼有些元件依賴可以不寫version版本號就能依賴成功,而有些卻不行呢?

如下面的lombok和mybatis-plus-boot-starter

<!--		lombok依賴-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
<!--		mybatis-plus依賴-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.0.5</version>
		</dependency>

這是因為maven繼承的原因,父模組整合了公共的maven依賴,只需要依賴GA即可。

2、1、MyBatis-Plus如何幫我們把SQL寫好了,我們的UserDao介面在實際執行中是如何建立實現類來具體實現我們想要的方法的?

參考

MyBatis-Plus官網

MyBatis-Plus教程——狂神

MyBatis、Spring、SpringMVC

基於3.0.5及3.4.2版本

擴充套件

Q: UTC和GMT的區別?

A:UTC是我們現在用的時間標準,GMT是老的時間計量標準。UTC是根據原子鐘來計算時間,而GMT是根據地球的自轉和公轉來計算時間,也就是太陽每天經過位於英國倫敦郊區的皇家格林威治天文臺的時間就是中午12點,。由於現在世界上最精確的原子鐘50億年才會誤差1秒(最精確原子鐘問世:50億年誤差一秒),可以說非常精確。而GMT因為是根據地球的轉動來計算時間的,而地球的自轉正在緩速變慢,所以使用GMT的話,總有一天,打個比方,中午12點,並不是一天太陽當頭照的時候,很可能就是早上或者晚上了。所以說UTC更加精確。

[參考]( UTC和GMT什麼關係? - 知乎使用者的回答 - 知乎 https://www.zhihu.com/question/27052407/answer/56923233 )

相關文章