1. 程式人生 > 實用技巧 >MyBatis-Plus 學習筆記

MyBatis-Plus 學習筆記

MyBatis-Plus 學習筆記

目錄

1. 簡介

1.1 介紹

MyBatus-Plus(簡稱 MP)是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只增強不做改變,為簡化開發,提高效率而生。

就像 魂鬥羅 中的 1P、2P,基友搭配,效率翻倍。

官網:https://mp.baomidou.com/

github:https://github.com/baomidou/mybatis-plus

1.2 特性

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

1.3 框架結構

2. 快速開始

環境:

JDK 1.8

Maven 3.6.1

mysql 5.7.27

MyBtisPlus 3.3.2 (MP 3.3.X 版本相比於以前的版本改動了不少,使用時請注意版本號)

idea 2019.3

  1. 建立一個 SpringBoot 工程專案

  2. 引入依賴

    <dependencies>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.23</version>
        </dependency>
        <!-- mybatis plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    
        <!-- SpringBoot 相關依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <!-- SpringBoot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
    </parent>
    
    <build>
        <!-- 外掛 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <!-- 靜態資源解析 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    
  3. 建立測試所需要的表,並插入測試資料

    # 建立表
    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 '郵箱',
    	PRIMARY KEY (id)
    );
    
    #插入資料
    DELETE FROM user;
    
    INSERT INTO user (id, name, age, email) VALUES
    (1, 'Jone', 18, '[email protected]'),
    (2, 'Jack', 20, '[email protected]'),
    (3, 'Tom', 28, '[email protected]'),
    (4, 'Sandy', 21, '[email protected]'),
    (5, 'Billie', 24, '[email protected]');
    
  4. 編寫實體類

    這裡使用了 Lombok

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        private Long id;
        private String name;
        private Integer age;
        private String email;
    
    }
    
  5. 配置資料來源

    在 application.yml 中配置資料來源,這裡使用的是 Druid 資料來源,也可以使用預設的資料來源

    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切換成 Druid 資料來源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己繫結
        #druid 資料來源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置監控統計攔截的filters,stat:監控統計、log4j:日誌記錄、wall:防禦sql注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  6. 建立 mapper 介面

    建立 mapper 介面,並繼承 BaseMapper 介面,規定泛型型別

    @Repository // 將 mapper 註冊到 Spring 容器中
    public interface UserMapper extends BaseMapper<User> {
    }
    
  7. 在主啟動類中掃描介面

    在主啟動類新增 @MapperScan() 註解,來掃描我們剛剛建立的 mapper 介面

    @MapperScan("com.xp.mapper")  // 掃描 mapper 介面
    @SpringBootApplication
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    
  8. 測試

    編寫測試類

    @SpringBootTest
    public class ApplicationTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void test(){
            // UserMapper 中的 selectList() 方法的引數為 MyBatisPlus 內建的的條件封裝器 wrapper,所以不填寫就是無任何條件
            List<User> userList = userMapper.selectList(null);
            for (User user : userList) {
                System.out.println(user);
            }
        }
    
    }
    

    測試結果如下:

  9. 配置日誌

    如果我們想要知道 sql 語句的執行情況,我們可以配置日誌。想要開啟日誌功能,只需配置如下配置即可:

    其中 log-impl 的值可以是其他的日誌,不一定是我下面的,也可以配置 log4j

    mybatis-plus:
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    

3. 註解

除了 MyBatis 原本的註解,MP 還有一些自己的註解

3.1 @TableName

  • 描述:表名註解
屬性 型別 必須指定 預設值 描述
value String "" 表名
schema String "" schema
keepGlobalPrefix boolean false 是否保持使用全域性的 tablePrefix 的值(如果設定了全域性 tablePrefix 且自行設定了 value 的值)
resultMap String "" xml 中 resultMap 的 id
autoResultMap boolean false 是否自動構建 resultMap 並使用(如果設定 resultMap 則不會進行 resultMap 的自動構建並注入)

關於 ‘autoResultMap’ 的說明

mp 會自動構建一個 ResultMap 並注入到 mybatis 裡(一般用不上)。下面講兩句:因為 mp 底層是 MyBatis ,所以一些 MyBatis 的嘗試你要知道,mp 只是幫你注入了常用 crud 到 MyBatis 裡,注入之前可以說是動態的(根據你entity的欄位以及註解變化而變化),但是注入之後是靜態的(等於你寫在xml的東西)而對於直接指定 typeHandler,MyBatis 只支援你寫在兩個地方:

  1. 定義在 resultMap 裡,只作用於 select 查詢的返回結果封裝
  2. 定義在 insertupdate sql 的 {property} 裡的 property 後面(例:#{property,typehandler-xxx.xxx.xxx}),只作用域 設定值 。而除了這兩種直接指定 typeHandler,MyBatis 有一個全域性的掃描你自己的 typeHandler 包的配置,這是根據你的 property 的型別去找 typeHandler 並使用。

3.2 @Tableld

  • 描述:主鍵註解
屬性 型別 必須指定 預設值 描述
value String "" 主鍵欄位名
type Enum IdType.NONE 主鍵型別

IdType

描述
AUTO 資料庫 ID 自增
NONE 無狀態,該型別為未設定主鍵型別(註解裡等於跟隨全域性,全局裡約等於 INPUT)
INPUT insert 前自行 set 主鍵值
ASSIGN_ID 分配 ID(主鍵型別為 Number (Long和Integer)或 String)(since 3.3.0),使用介面 IdentifierGenerator 的方法 nextId (預設實現類為 DefaultIdentifierGenerator 雪花演算法)
ASSIGN_UUID 分配 UUID ,主鍵型別為 String(since 3.3.0),使用介面IdentifierGenerator 的方法 nextUUID(預設default方法)
ID_WORKER 分散式全域性唯一 ID 長整性型別(please use ASSIGN_ID
UUID 32 位 UUID 字串(please use ASSIGN_UUID
ID_WORKER_STR 分散式全域性唯一 ID 字串型別(please user ASSIGN_ID

3.3@TableField

  • 描述:欄位註解(非主鍵)
屬性 型別 必須指定 預設值 描述
value String "" 資料庫欄位名
el String "" 對映原生 #{...} 邏輯,相當於寫在 xml 裡的 #{...}
exist boolean true 是否位資料庫表字段
condition String "" 欄位 where 實體查詢比較條件,有值設定則按設定的值為準,沒有則為預設全域性的 %s=#{%s}
update String "" 欄位 update set 部分注入。例如:update ="%s+1":表示更新時會 set version = version +1(該屬性優先順序高於 el 屬性)
insertStrategy Enum N DEFAULT 舉例 NOT_NULL: insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
updateStrategy Enum N DEFAULT 舉例:IGNORED: update table_a set column=#{columnProperty}
whereStrategy Enum N DEFAULT 舉例:NOT_EMPTY: where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
fill Enum FieldFill.DEAFULT 欄位自動填充策略
select boolean true 是否進行 select 查詢
keepGloabalFormat boolean false 是否保持使用全域性的 format 進行處理
jdbcType JdbcType JdbcType.UNDEFINED JDBC型別(該預設值不代表會按照該值生效)
typeHandler Class<? extend TypeHandler> UnknownTypeHandler.class 型別處理器(該預設值不代表會按照該值生效)
numericScale String "" 指定小數點後保留機位

關於 ‘jdbcType’ 和 ‘numerucScale’ 的說明

numericScale 只生肖與 update 的 sql。jdbcTypetypeHandler 如果不配合 @TableName#autoResultMap = true 一起使用,也只生效於 update 的 sql。對於 typeHandler 如果你的欄位型別和 set 進去的型別為 equals 關係,則只需要讓你的 typeHandler 讓 MyBatis 載入到即可,不需要使用註解

FieldStrategy

描述
IGNORED 忽略判斷
NOT_NULL 非 NULL 判斷
NOT_EMPTY 非空判斷(只對字串型別欄位。其他型別欄位依然為非NULL判斷)
DEFAULT 追隨全域性配置

FieldFill

描述
DEFAULT 預設不處理
INSERT 插入時填充欄位
UPDATE 更新時填充欄位
INSERT_UPDATE 插入和更新時填充欄位

3.4 @Version

  • 描述:樂觀鎖註解。標記 @Version 在欄位上

3.5 @EnumValue

  • 描述:通過列舉類註解(註解在列舉類欄位上)

3.6 @TableLogic

  • 描述:表字段邏輯處理註解(邏輯刪除)
屬性 型別 必須指定 預設值 描述
value String "" 邏輯未刪除值
delval String "" 邏輯刪除值

3.7 @SqlParser

  • 描述:租戶註解,支援 method 上以及 mapper 介面上
屬性 型別 必須指定 預設值 描述
filter boolean false true:表示過濾 SQL 解析,即不會進入 ISqlParser 解析鏈,否則會緊解析鏈並追加例如 tenant_id 等條件

3.8 @KeySequence

  • 描述:序列主鍵策略 oracle
  • 屬性:value、resultMap
屬性 型別 必須指定 預設值 描述
value String "" 序列名
clazz Class Long.class id的型別,可以指定 String.class ,這樣返回的 Sequence 值是字串 “1”

4. CRUD

4.1 增加

MP 中提供了插入的方法 insert()

@SpringBootTest
public class ApplicationTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void test(){

        System.out.println(userMapper.insert(new User(null, "張三", 18, "[email protected]")));

    }

}

檢視我們控制檯列印的日誌

可以發現,這 id 是我們看不懂的東西,可不是我們真正想要的(主鍵自增)

那麼這個id是怎麼生成的呢?

在我們上面的註解中的 @Tableld 有寫道,預設是 IdType.NONE。它是自動生成全域性唯一的id。

如果我們需要修改成為自增,只需在主鍵上加@Tableld設定 type 修改成 IdType.AUTO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
	// 設定type=IdType.AUTO,讓主鍵自增
    @TableId(type=IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;

}

需要注意的是,我們要先確定我們的表的主鍵是已經設定自增的,不然會報錯

4.2 刪除

1. 普通刪除

// 普通刪除
@Test
void delete(){
    // 根據id刪除
    userMapper.deleteById(7L);
    // 批量刪除
    userMapper.deleteBatchIds(Arrays.asList(4L,5L,6L));
    // 通過 map 刪除
    Map<String, Object> map = new HashMap<>();
    map.put("name","李四");
    userMapper.deleteByMap(map);
}

2. 邏輯刪除

首先,我們先在資料庫的表中增加 deleted 欄位,預設值為0 .(實際開發中是不允許修改資料庫的,只是為了測試方便)

修改實體類,讓資料庫的表和實體類一一對應,並在 deleted 上新增 @TableLogic 註解(3.3.0版本後可以不需要增加這個註解,只需在配置檔案中開啟即可)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @TableId(type=IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic
    private Integer deleted;

}

在 application.yml 中配置邏輯刪除

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全域性邏輯刪除的實體欄位名(since 3.3.0,配置後可以不需要在實體類欄位上增加註解)
      logic-delete-value: 1 # 邏輯已刪除的值 (預設為1)
      logic-not-delete-value: 0 # 邏輯未刪除的值 (預設為0)

測試

// 邏輯刪除
@Test
void deleteLogic(){
    // 通過 id 進行邏輯刪除
    userMapper.deleteById(2);
    // 批量刪除
    userMapper.deleteBatchIds(Arrays.asList(1,8,2));
    // 通過 map 刪除
    Map<String, Object> map = new HashMap<>();
    map.put("name","李四");
    userMapper.deleteByMap(map);
    // 查詢使用者,檢視查詢語句是否發生了變化
    System.out.println(userMapper.selectById(2));
}

3. 說明

只對自動注入的 sql 起效

  • 插入:不作限制
  • 查詢:追加 where 條件過濾掉已刪除資料,且使用 wrapper.entity 生成的 where 條件會忽略該欄位
  • 更新:追加 where 條件防止更新到已刪除資料,且使用 wrapper.entity 生成的 where 條件會忽略該欄位
  • 刪除:轉變為更新

例如:

  • 刪除:update user set deleted=1 where id=1 and deleted=0
  • 查詢:select id,name,deleted from user where deleted=0

欄位型別支援說明:

  • 支援所有資料型別(推薦使用 Integer,boolean,LocalDateTime
  • 如果資料庫欄位使用 dateTime,邏輯未刪除值和已刪除值支援配置為字串 null ,另一個值支援配置為函式來獲取值如 now()

附錄:

  • 邏輯刪除是為了方便資料恢復和保護資料本身價值等等的一種方案,但實際就是刪除。
  • 如果你需要頻繁查出來看就不應使用邏輯刪除,而是以一個狀態去表示

4. 常見問題

  1. 如何 insert?

    1. 欄位在資料庫定義預設值(推薦)
    2. insert 前自己 set 值
    3. 使用自動填充功能
  2. 刪除介面自動填充功能失效

    1. 使用 update 方法並 UpdateWrapper.set(column,value)(推薦)

    2. 使用 update 方法並 UpdateWrapper.setSql("column=value")

    3. 使用 Sql 注入器 注入

      com.baomidou.mybatisplus.extension.injector.methods.LogicDeleteByIdWithFill(推薦)

4.3 修改

1. 根據 id 進行修改

@SpringBootTest
public class ApplicationTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void test(){
        System.out.println(userMapper.updateById(new User(6L, "李四", 3, null)));
    }

}

檢視控制檯日誌輸出,我們可以發現 MP 的插入方法是根據條件使用動態 SQL 實現的,這樣就節省了我們寫動態 SQL 的時間

2. 條件構造器修改

MP 提供了修改操作的條件構造器 UpdateWrapper

@Test
void queryWrapper(){
    // 使用 updateWrapper, sql中的 where 條件在 updateWrapper 中的 Entity 中設定,也即是.setEntity()這個方法設定查詢條件
    // 這裡的update方法的第一個引數是設定 set 的值,可以為空, updateWrapper 中也可以設定 set 的值
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
    // 這裡,setEntity(T entity) 是用來構建 where 後面的條件,若不設定則預設是更改所有的資料
    updateWrapper.set("name","李四").setEntity(new User().setName("張三"));
    userMapper.update(null,updateWrapper);
}

我們檢視控制檯日誌,可以看到 sql 語句和這兩個引數的對應關係

4.4 查詢

1. 普通查詢

@Test
void select(){
    // 根據id查詢使用者
    User user = userMapper.selectById(1);
    System.out.println(user);

    // 批量查詢
    List<User> userList = userMapper.selectBatchIds(Arrays.asList(1, 2, 4, 5));
    userList.forEach(System.out::println);

    // 條件查詢之一 map
    Map<String, Object> map = new HashMap<>();
    map.put("name","李四");
    map.put("age","3");
    List<User> users = userMapper.selectByMap(map);
    users.forEach(System.out::println);
}

2. 分頁查詢

要進行分頁查詢,我們需要先配置 MP 的分頁外掛,並將 MP 的分頁外掛註冊到 Spring 容器中

在我們之前寫的 MyBatisPlus 配置類 MyBatisPlusConfig 類中增加如下程式碼:

MyBatisPlusConfig

@Bean
PaginationInterceptor paginationInterceptor(){
    PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
    // 設定請求的頁面大於最大頁後操作, true調回到首頁,false 繼續請求  預設false
    // paginationInterceptor.setOverflow(false);
    // 設定最大單頁限制數量,預設 500 條,-1 不受限制
    // paginationInterceptor.setLimit(500);
    // 開啟 count 的 join 優化,只針對部分 left join
    paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
    return paginationInterceptor;
}

測試

// 分頁查詢
@Test
void selectPage(){
    // 普通分頁查詢,Page構造器的第一個引數表示第幾頁,第二個引數表示每頁最多多少條資料
    Page<User> page = new Page<>(2,5);
    // 進行分頁查詢
    Page<User> userPage = userMapper.selectPage(page, null);
    // 獲取分頁記錄
    List<User> userList = userPage.getRecords();
    userList.forEach(System.out::println);
    // 獲取總記錄數
    System.out.println("總頁數:"+page.getTotal());
    // 獲取總頁數
    long pages = page.getPages();
    System.out.println("總記錄數:"+pages);
    // 每頁展示最大記錄條數
    System.out.println("每頁展示最大記錄除錯:"+page.getSize());
}

3. 條件構造器查詢

MP 中提供了查詢的條件構造器 QueryWrapper

@Test
void queryWrapper(){
    // 建立 QueryWrapper 物件
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    // 構造條件,wrapper 是鏈式程式設計,所以可以構造很多的條件.這裡的查詢條件是 name=張三 且 age>3 
    wrapper.eq("name","張三").gt("age",3);
    // 查詢結果
    List<User> userList = userMapper.selectList(wrapper);
    userList.forEach(System.out::println);
}

5. 自動填充

根據阿里巴巴開發手冊:所有的資料庫表都應該有 gmt_create、gmt_modified 兩個欄位。且更新資料表記錄時,必須同時更新記錄對應的 gmt_modified 欄位值為當前時間。

MP 已經幫我們做好了自動填充的操作。

5.1 修改表結構

在我們原本的 User 表中增加 gmt_create 和 gmt_modified 兩個欄位

5.2 同步實體類

同步實體類,並在實體類上增加註解

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @TableId(type=IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date modifiedTime;

}

上面的註解介紹中已經有寫道,@TableField 這個註解中 fill 屬性可以設定自動填充策略

FieldFill 中有4個自動填充策略

public enum FieldFill {
    DEFAULT,		// 預設不處理
    INSERT,			// 插入時自動填充填充
    UPDATE,			// 更新時自動填充
    INSERT_UPDATE;	// 插入或更新時自動填充

    private FieldFill() {
    }
}

5.3 編寫填充策略

我們自定義一個元資料處理器 MyMetaObjectHandler,實現 MetaObjectHandler 介面

@Slf4j      // 使用日誌
@Component  // 將我們自定義的元資料處理器註冊到 Spring 容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert ");
        // 設定插入的填充 引數分別時:第一個為原資料物件,第二個為填充的欄位,第三個為填充的內容的Class物件,第四個是填充的內容
        this.strictInsertFill(metaObject,"createTime",Date.class,new Date());
        this.strictInsertFill(metaObject,"modifiedTime",Date.class,new Date());
        // fillStrategy 這個也可以設定自動填充,但是有bug 需要升級到 3.3.1.8-SNAPSHOT版本後
        // this.fillStrategy(metaObject,"modifiedTime",new Date());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update ");
        // 設定修改的填充
        this.strictUpdateFill(metaObject,"modifiedTime",Date.class,new Date());
   		// fillStrategy 這個也可以設定自動填充,但是有bug 需要升級到 3.3.1.8-SNAPSHOT版本後
        // this.fillStrategy(metaObject,"modifiedTime",Date.class,new Date());
    }
}

5.4 測試

我們先執行增加操作,看下增加操作是否自動填充了

@SpringBootTest
public class ApplicationTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void test(){
        System.out.println(userMapper.insert(new User(null, "張三", 18, "[email protected]",null,null)));
    }

}

可以發現,我們自定義的元資料處理器已經生效了,並且在執行插入操作的時候,createTime 和 modifiedTime 兩個欄位都自動填充了現在的時間

我們再執行修改操作

@SpringBootTest
public class ApplicationTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void test(){
        System.out.println(userMapper.updateById(new User(3L, "李四", 3, null,null,null))));
    }

}

可以發現我們自定義的元資料處理器已經生效,而且自動更新了 modifiedTime 欄位的修改時間

5.5 注意事項

  • 欄位必須宣告 @TableField 註解,屬性 fill選擇對應策略,該宣告告知 MyBatis-Plus 需要預留注入 sql 欄位
  • 填充處理器 MyMetaObjectHandler 在 SpringBoot 中需要宣告 @Component@Bean 注入
  • 要想根據註解 FieldFill.xxx欄位名 以及 欄位型別來取分必須使用父類的 strictInsertFill 或者 strictUpdateFill 方法
  • 不需要根據任何來取分可以使用父類的 fillStrategy 方法
  • 使用 update(T entity, Wrapper<T> updateWrapper) 時,想要自動填充,則第一個 entity 不能為空,否則則無法自動填充

6. 樂觀鎖

在面試過程中,我們經常會被問到樂觀鎖,悲觀鎖。

6.1 什麼是樂觀鎖,什麼是悲觀鎖

什麼是悲觀鎖(Pessimistic Lock)

當要對資料庫中的一條資料進行修改的時候,為了避免同時被其他人修改,最好的辦法就是直接對該資料進行加鎖以防止併發。這種藉助資料庫所機制,在修改資料之前先鎖定,再修改的方式被稱之為悲觀併發控制【又名“悲觀鎖”,Perssimistic ,Concurrency Control,縮寫“PCC"】

什麼是樂觀鎖(Optimistic Locking)

樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設資料一般情況下不會造成衝突,所以再資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則返回給使用者錯誤的資訊,讓使用者決定如何去做。

6.2 樂觀鎖外掛

MP 提供了樂觀鎖外掛

意圖:當要更新一條記錄的時候,希望這條記錄沒有被別人更新

樂觀鎖的實現方式:

  • 取出記錄時,獲取當前 version
  • 更新時,帶上這個 version
  • 執行更新時, set version = newVersion where version = oldVersion
  • 如果 version 不對,就更更新失敗

比如: 更新時的 version=1

update user set name='xp',version = version +1 where id=1 and version=1

如果執行更新操作時,資料已經被修改,即 version 不再是原來的 version =1,則會更新失敗

6.2.1 外掛配置

  1. 修改資料表

    將我們之前的 User 表新增 version 欄位,並設定預設為1

  2. 將樂觀鎖外掛註冊到 Spring 容器中

    建立一個 MyBatisPlusConfig 配置類

    @Configuration
    public class MyBatisPlusConfig {
    
        @Bean
        OptimisticLockerInterceptor optimisticLockerInterceptor(){
            return new OptimisticLockerInterceptor();
        }
    
    }
    
  3. 註解欄位(必須要)

    修改實體類,給 version 增加註解

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        @TableId(type=IdType.AUTO)
        private Long id;
        private String name;
        private Integer age;
        private String email;
        @TableField(fill = FieldFill.INSERT)
        private Date createTime;
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Date modifiedTime;
        @Version
        private Integer version;
    
    }
    

    特別說明

    • 支援的資料型別只有:int,Integer,long,Long,Date,TimeStamp,LocalDateTime
    • 整數型別下 newVersion = oldVersion + 1
    • newVersion 會回寫到 entity
    • 僅支援 updateById(id)update(entity,wrapper)方法
    • update(entity,wrapper) 方法下,wrapper 不能再複用!!
  4. 測試

    先模擬樂觀鎖更新成功,單執行緒時

    // 模擬樂觀鎖成功
    @Test
    void test1(){
        // 1.查詢使用者資訊
        User user = userMapper.selectById(2);
        System.out.println(user);
        // 2.修改使用者資訊
        user.setName("xp01");
        user.setId(2L);
        user.setAge(3);
        // 3.執行更新操作
        userMapper.updateById(user);
    }
    

    執行後控制檯輸出如下:

    可以看到,我們原本的版本號是1,執行更新操作後,MP 幫我們將版本號進行 +1 的操作

    模擬樂觀鎖失敗,多執行緒下

    // 模擬樂觀鎖失敗,多執行緒下
    @Test
    void test2(){
        // 執行緒1
        User user = userMapper.selectById(2);
        user.setName("xp111");
        user.setId(2L);
        user.setAge(1);
        System.out.println(user);
    
        // 模擬另一個執行緒執行插隊操作
        User user1 = userMapper.selectById(2);
        user1.setName("xp222");
        user1.setId(2L);
        user1.setAge(2);
        System.out.println(user1);
        userMapper.updateById(user1);
    
        // 樂觀鎖失敗
        userMapper.updateById(user);
    }
    

    執行後控制檯輸出如下:

​ 我們可以發現,線上程2插隊執行更新操作後,執行緒1 執行更新操作時,MP幫我們自動加上了樂觀鎖,所以執行緒1 更新資料失敗了

7. 效能分析外掛

我們平時的開發中,會遇到一些慢 SQL。

MP 提供了效能分析外掛,如果 sql 執行時間超過這個時間就停止執行!

但在 MP 的 3.2 版本後移除了效能分析外掛

這裡就基於 p6spy 外掛來學習,想要是要的話得降版本使用

  1. 引入依賴

    <!-- p6spy 效能分析外掛 -->
    <dependency>
        <groupId>p6spy</groupId>
        <artifactId>p6spy</artifactId>
        <version>3.9.1</version>
    </dependency>
    
  2. 配置 p6spy

    在 application.yml 配置中將資料來源的 driver-class-name 改成 p6spy 提供的驅動

    spring:
      datasource:
        driver-class-name: com.p6spy.engine.spy.P6SpyDriver
        url: jdbc:p6spy:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
    
    
    

    在 resources 目錄下建立 spy.properties 檔案,並配置如下內容:

    #3.2.1以上使用
    modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
    #3.2.1以下使用或者不配置
    #modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
    # 自定義日誌列印
    logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
    #日誌輸出到控制檯
    appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
    # 使用日誌系統記錄 sql
    #appender=com.p6spy.engine.spy.appender.Slf4JLogger
    # 設定 p6spy driver 代理
    deregisterdrivers=true
    # 取消JDBC URL字首
    useprefix=true
    # 配置記錄 Log 例外,可去掉的結果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
    excludecategories=info,debug,result,commit,resultset
    # 日期格式
    dateformat=yyyy-MM-dd HH:mm:ss
    # 實際驅動可多個
    #driverlist=org.h2.Driver
    # 是否開啟慢SQL記錄
    outagedetection=true
    # 慢SQL記錄標準 2 秒
    outagedetectioninterval=2
    

    注意

    • driver-class-name 為 p6spy提供的驅動類
    • url 字首為 jdbc:p6spy 跟著冒號為對應資料庫連線地址
    • 打印出 sql 為null,在 excludecategories 增加 commit
    • 批量操作不列印 sql,去除 excludecategories 中的 batch
    • 批量操作列印重複的問題請使用 MybatisPlusLogFactory (3.2.1新增)
    • 該外掛有效能損耗,不建議生產環境使用
  3. 簡單使用

    執行之前查詢語句,檢視控制檯日誌輸出

    控制檯中輸出了sql語句以及執行時間,查詢資料好像沒有找到慢 SQL 可以是小輸,所以這裡無法演示慢SQL 執行時的控制檯輸出。MP 以前自帶的效能分析外掛慢SQL標準是可以精確到毫秒級別的

8. 條件構造器

8.1 說明

  • 以下出現的第一個入參 boolean condition 表示該條件是否加入最後生成的 sql 中
  • 以下程式碼塊內的那個方法均為從上往下補全個別 boolean 型別的入參,預設為 true
  • 以下出現的泛型 Param 均為 wrapper 的子類例項(均具有 AbstractWrapper的所有方法)
  • 以下方法在入參中出現的 R 為泛型,在普通 wrapper 中式 String ,在 LambdaWrapper 中是函式(例:Entity::getIdEntity為實體類,getId為欄位 idget(Method)
  • 以下方法入參中 R column 均表示資料庫欄位。當 R 具體型別為 String 時則為資料庫欄位名(欄位名是資料庫關鍵字的自己用轉義符包裹!)而不是具體實體類資料欄位名!!!,另當 R 具體型別為 SFunction 時專案 runtime 不支援 eclipse 自家的編譯器!!!
  • 以下舉例為使用 普通 wrapper,入參為 maplist 的均以 json 形式表現!
  • 使用中如果入參 map 或者 list 為空 ,則不會加入最後生成的 sql 中!!!

8.2 警告

不支援以及不贊成在 RPC 呼叫中把 Wrapper 進行傳輸

  1. wrapper 很重
  2. 傳輸 wrapper 可以類比為你的 controller 用 map 接受值(開發一時爽,維護火葬場)
  3. 正確的 RPC 呼叫模式時寫一個 DTO 進行傳輸,被呼叫方再根據 DTO 執行響應的操作
  4. 我們拒絕接收任何關於 RPC 傳輸 Wrapper 報錯相關的 issue 甚至 pr

8.3 AbstractWrapper

說明

QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapepr)的父類用於生成 sql 的 where 條件,entity 屬性也用於生成 sql 的 where 條件

注意:entity 生成的 where 條件與使用各個 api 生成的 where 條件沒有任何關聯行為

allEq

allEq(Map<R, V> params)
allEq(Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
  • 全部 eq(或個別isNull)

個別引數說明

paramskey為資料庫欄位名,value為欄位值

null2IsNull:為 true 則再 mapvaluenull 時呼叫 isNull 方法, 為 false 時則忽略valuenull

  • 例1: allEq({id:1,name:"老王",age:null})--->id = 1 and name = '老王' and age is null
  • 例2: allEq({id:1,name:"老王",age:null}, false)--->id = 1 and name = '老王'
allEq(BiPredicate<R, V> filter, Map<R, V> params)
allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull) 

個別引數說明

filter:過濾函式,是否允許欄位傳入比對條件中

paramsnull2IsNull:同上

  • 例1: allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null})--->name = '老王' and age is null
  • 例2: allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null}, false)--->name = '老王'

eq

eq(R column, Object val)
eq(boolean condition, R column, Object val)
  • 等於 =
  • 例: eq("name","老王")--->name='老王'

ne

ne(R column, Object val)
ne(boolean condition, R column, Object val)
  • 不等於 <>
  • 例: ne("name", "老王")--->name <> '老王'

gt

ge(R column, Object val)
ge(boolean condition, R column, Object val)
  • 大於 >
  • 例: gt("age", 18)--->age > 18

ge

ge(R column, Object val)
ge(boolean condition, R column, Object val)
  • 大於等於 >=
  • 例: ge("age", 18)--->age >= 18

lt

lt(R column, Object val)
lt(boolean condition, R column, Object val)
  • 小於 <
  • 例: lt("age", 18)--->age < 18

le

le(R column, Object val)
le(boolean condition, R column, Object val)
  • 小於等於 <=
  • 例: le("age", 18)--->age <= 18

between

between(R column, Object val1, Object val2)
between(boolean condition, R column, Object val1, Object val2)
  • BETWEEN 值1 AND 值2
  • 例: between("age", 18, 30)--->age between 18 and 30

noBetween

notBetween(R column, Object val1, Object val2)
notBetween(boolean condition, R column, Object val1, Object val2)
  • NOT BETWEEN 值1 AND 值2
  • 例: notBetween("age", 18, 30)--->age not between 18 and 30

like

like(R column, Object val)
like(boolean condition, R column, Object val)
  • LIKE '%值%'
  • 例: like("name", "王")--->name like '%王%'

notLike

notLike(R column, Object val)
notLike(boolean condition, R column, Object val)
  • NOT LIKE '%值%'
  • 例: notLike("name", "王")--->name not like '%王%'

LikeLeft

likeLeft(R column, Object val)
likeLeft(boolean condition, R column, Object val)
  • LIKE '%值'
  • 例: likeLeft("name", "王")--->name like '%王'

likeRight

likeRight(R column, Object val)
likeRight(boolean condition, R column, Object val)
  • LIKE '值%'
  • 例: likeRight("name", "王")--->name like '王%'

isNull

isNull(R column)
isNull(boolean condition, R column)
  • 欄位 IS NULL
  • 例: isNull("name")--->name is null

isNotNull

isNotNull(R column)
isNotNull(boolean condition, R column)
  • 欄位 IS NOT NULL
  • 例: isNotNull("name")--->name is not null

in

in(R column, Collection<?> value)
in(boolean condition, R column, Collection<?> value)
  • 欄位 IN (value.get(0), value.get(1), ...)
  • 例: in("age",{1,2,3})--->age in (1,2,3)
in(R column, Object... values)
in(boolean condition, R column, Object... values)
  • 欄位 IN (v0, v1, ...)
  • 例: in("age", 1, 2, 3)--->age in (1,2,3)

notIn

notIn(R column, Collection<?> value)
notIn(boolean condition, R column, Collection<?> value)
  • 欄位 NOT IN (value.get(0), value.get(1), ...)
  • 例: notIn("age",{1,2,3})--->age not in (1,2,3)
notIn(R column, Object... values)
notIn(boolean condition, R column, Object... values)
  • 欄位 NOT IN (v0, v1, ...)
  • 例: notIn("age", 1, 2, 3)--->age not in (1,2,3)

inSql

inSql(R column, String inValue)
inSql(boolean condition, R column, String inValue)
  • 欄位 IN ( sql語句 )
  • 例: inSql("age", "1,2,3,4,5,6")--->age in (1,2,3,4,5,6)
  • 例: inSql("id", "select id from table where id < 3")--->id in (select id from table where id < 3)

notInSql

notInSql(R column, String inValue)
notInSql(boolean condition, R column, String inValue)
  • 欄位 NOT IN ( sql語句 )
  • 例: notInSql("age", "1,2,3,4,5,6")--->age not in (1,2,3,4,5,6)
  • 例: notInSql("id", "select id from table where id < 3")--->id not in (select id from table where id < 3)

groupBy

groupBy(R... columns)
groupBy(boolean condition, R... columns)
  • 分組:GROUP BY 欄位, ...
  • 例: groupBy("id", "name")--->group by id,name

orderByAsc

orderByAsc(R... columns)
orderByAsc(boolean condition, R... columns)
  • 排序:ORDER BY 欄位, ... ASC
  • 例: orderByAsc("id", "name")--->order by id ASC,name ASC

orderByDesc

orderByDesc(R... columns)
orderByDesc(boolean condition, R... columns)
  • 排序:ORDER BY 欄位, ... DESC
  • 例: orderByDesc("id", "name")--->order by id DESC,name DESC

orderBy

orderBy(boolean condition, boolean isAsc, R... columns)
  • 排序:ORDER BY 欄位, ...
  • 例: orderBy(true, true, "id", "name")--->order by id ASC,name ASC

having

having(String sqlHaving, Object... params)
having(boolean condition, String sqlHaving, Object... params)
  • HAVING ( sql語句 )
  • 例: having("sum(age) > 10")--->having sum(age) > 10
  • 例: having("sum(age) > {0}", 11)--->having sum(age) > 11

func

func(Consumer<Children> consumer)
func(boolean condition, Consumer<Children> consumer)
  • func 方法(主要方便在出現if...else下呼叫不同方法能不斷鏈)
  • 例: func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})

or

or()
or(boolean condition)

拼接 OR

注意事項

主動呼叫 or 表示緊接著下一個方法不是用 and 連線!(不呼叫 or 則預設為使用 and 連線)

  • 例: eq("id",1).or().eq("name","老王")--->id = 1 or name = '老王'
or(Consumer<Param> consumer)
or(boolean condition, Consumer<Param> consumer)
  • OR 巢狀
  • 例: or(i -> i.eq("name", "李白").ne("status", "活著"))--->or (name = '李白' and status <> '活著')

and

and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)
  • AND 巢狀
  • 例: and(i -> i.eq("name", "李白").ne("status", "活著"))--->and (name = '李白' and status <> '活著')

nest

nested(Consumer<Param> consumer)
nested(boolean condition, Consumer<Param> consumer)
  • 正常巢狀 不帶 AND 或者 OR
  • 例: nested(i -> i.eq("name", "李白").ne("status", "活著"))--->(name = '李白' and status <> '活著')

apply

apply(String applySql, Object... params)
apply(boolean condition, String applySql, Object... params)
  • 拼接 sql

    注意事項

    該方法可用於資料庫函式動態入參得 params 對應得前面 applySql 內部得(index)部分。這樣是不會有 sql 注入風險的,反之會有!

  • 例: apply("id = 1")--->id = 1

  • 例: apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")

  • 例: apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08")--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")

last

last(String lastSql)
last(boolean condition, String lastSql)
  • 無視優化規則直接拼接到 sql 的最後

    注意事項

    只能呼叫一次,多次呼叫以最後一次為準,有 sql 注入的風險,請謹慎使用

  • 例: last("limit 1")

exists

exists(String existsSql)
exists(boolean condition, String existsSql)
  • 拼接 EXISTS ( sql語句 )
  • 例: exists("select id from table where age = 1")--->exists (select id from table where age = 1)

notExists

notExists(String notExistsSql)
notExists(boolean condition, String notExistsSql)
  • 拼接 NOT EXISTS ( sql語句 )
  • 例: notExists("select id from table where age = 1")--->not exists (select id from table where age = 1)

QueryWrapper

說明

繼承自 AbstractWrapper,自身的內部屬性 entity 也用於生成 where 條件及 LambdaQueryWrapper ,可以通過 new QueryWrapper().lambda() 方法獲取

select

select(String... sqlSelect)
select(Predicate<TableFieldInfo> predicate)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)

設定查詢欄位

說明

以上方法為兩類

第二類方法為過濾器查詢欄位(主鍵除外),入參不包含 class 的呼叫前需要 wrapper 內的 entity 屬性有值!這兩類方法重複呼叫以最後一次為準

  • 例: select("id", "name", "age")
  • 例: select(i -> i.getProperty().startsWith("test"))

UpdateWrapper

說明

繼承自 AbstractWrapper,自身的內部屬性 entity 也用於生成 where 條件及 LambdaUpdateWrapper,可以通過 new UpdateWrapper().lambda() 方法獲取!

set

set(String column, Object val)
set(boolean condition, String column, Object val)
  • SQL SET 欄位
  • 例: set("name", "老李頭")
  • 例: set("name", "")--->資料庫欄位值變為空字串
  • 例: set("name", null)--->資料庫欄位值變為null

setSql

setSql(String sql)
  • 設定 SET 部分 SQL
  • 例: setSql("name = '老李頭'")

lambda

獲取 LambdaWrapper
QueryWrapper中是獲取LambdaQueryWrapper
UpdateWrapper中是獲取LambdaUpdateWrapper

使用 Wrapper 自定義 SQL

需求來源

再使用了 mybatis-plus 之後,自定義 SQL 的同時也想使用 wrapper 的便利應該怎麼辦?在 mybatis-plus 版本 3.0.7得到了完美解決。版本需要大於或等於 3.0.7 ,以下兩種方案取其一即可

Service.java

mysqlMapper.getAll(Wrappers.<MysqlData>lambdaQuery().eq(MysqlData::getGroup, 1));

Mapper.xml

<select id="getAll" resultType="MysqlData">
	SELECT * FROM mysql_data ${ew.customSqlSegment}
</select>

簡單示例

條件構造器的例子上面 CRUD 中有部分了,下面再寫幾個例子來找點感覺

@SpringBootTest
public class WrapperTest {

    @Autowired
    UserMapper userMapper;

    // 查詢 name 不為空的,並且郵箱不為空,年齡大於12的使用者
    @Test
    void test1(){
        // 建立 QueryWrapper
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // 構建條件,wrapper是鏈式程式設計,所以可以一直 .下去
        // isNotNull 有兩個過載方法,下面呼叫的這個方法的第一個引數表示的是資料庫表中的欄位名
        // gt 用來拼接 > 條件,第一個引數為資料庫表中的欄位名,第二個是 > 號後的值
        wrapper
                .isNotNull("name")
                .isNotNull("email")
                .gt("age",3);
        // 執行查詢操作,輸出查詢結果
        userMapper.selectList(wrapper).forEach(System.out::println);
    }

    // 查詢名字等於李四的,且年齡小於30的使用者
    @Test
    void test2(){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // eq 拼接 = 條件,第一個引數為資料庫表中的欄位名,第二個是 = 後的值
        // lt 拼接 < 條件,第一個引數為資料庫表中的欄位名,第二個是 《 後的值
        wrapper
                .eq("name","李四")
                .lt("age",30);
        // selectOne 查詢一個結果,如果返回多個結果,使用 selectMaps 或 selectList
        System.out.println(userMapper.selectOne(wrapper));
    }

    // 查詢年齡再 20-30 歲之間的使用者
    @Test
    void test3(){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // between 拼接 between and 條件,第一個引數表示資料庫中的欄位名,第二個引數為最小值,第三個引數為最大值
        wrapper.between("age",20,30);
        userMapper.selectList(wrapper).forEach(System.out::println);
    }

    // 模糊查詢
    // 名字當中不包含 張 的,且是 李 開頭的使用者
    @Test
    void test4(){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // notLike 拼接 not like 條件,第一個引數為資料庫表中的欄位名,第二個是 not like 後的值,它會自動拼接 %  % 比如這裡是 %張%
        // likeRight ,拼接 like 條件,第一個引數為資料庫表中的欄位名,第二個是 like後的值,它會再第二個引數後拼接 % 比如這裡是 李%
        wrapper
                .notLike("name","張")
                .likeRight("name","李");

        List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
        maps.forEach(System.out::println);
    }

    // 查詢使用者 id 小於2 的使用者
    @Test
    void test5(){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // inSql 在 in 條件後拼接 sql 語句,比如下面的這個執行後的語句為
        // SELECT id,name,age,email,create_time,modified_time,version,deleted FROM user WHERE deleted=0 AND (id IN (select id from user where id < 2))
        wrapper.inSql("id","select id from user where id < 2");
        userMapper.selectList(wrapper).forEach(System.out::println);
    }

    // 通過 id 進行排序
    @Test
    void test6(){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // orderByDesc sql 在末尾新增降序排序,引數是可變引數,引數都是資料庫中欄位名
        wrapper.orderByDesc("id");
        userMapper.selectList(wrapper).forEach(System.out::println);
    }

}

Wrapper 十分重要,我們需要執行復雜 sql 語句時需要使用到,可以多加練習,找到規律來使用 Wrapper ,這樣會讓你的開發更加快速高效

9. 程式碼生成器

AutoGenerator 是 MyBatis-Plus 的程式碼生成器,通過 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各個模組的程式碼,極大的提升了開發效率。

也就是說,MP 的程式碼生成器可以節省我們寫很多重複繁瑣的程式碼,快速搭建專案。

  1. 建立 user 表,或者使用我們剛剛的 user 表

    如果是用剛剛的 user 表,最好加上註釋,這樣我們等下自動生成 Swagger 註解時就可以看到很明顯的效果

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
      `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
      `age` int(11) NULL DEFAULT NULL COMMENT '年齡',
      `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '郵箱',
      `modified_time` datetime(6) NULL DEFAULT NULL COMMENT '修改時間',
      `create_time` datetime(6) NULL DEFAULT NULL COMMENT '建立時間',
      `version` int(1) NULL DEFAULT 1 COMMENT '版本號(樂觀鎖)',
      `deleted` int(1) NULL DEFAULT 0,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
    
    INSERT INTO `user` VALUES (1, '張三', 3, '[email protected]', '2020-07-31 14:10:17.902000', '2020-07-31 14:01:47.161240', 1, 0);
    INSERT INTO `user` VALUES (2, '李四', 18, '[email protected]', '2020-07-31 14:10:17.902000', '2020-07-31 14:01:47.161240', 4, 0);
    INSERT INTO `user` VALUES (8, '王五', 18, '[email protected]', '2020-07-31 14:10:17.902000', '2020-07-31 14:01:47.161240', 1, 0);
    
  2. 建立一個新的 SpringBoot 專案

  3. 引入依賴

    引入的核心依賴是 mybatis-plus-boot-startermysql-connector-javamybatis-plus-generatorvelocity-engine-core。只要存在這四個依賴,則可以使用程式碼生成器。資料來源可以使用 Spring預設的,這裡使用 Druid。資料庫連線驅動也不一定是要MySQL的。

    <dependencies>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.23</version>
        </dependency>
        <!-- mybatis plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!-- swagger-ui -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!-- swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!-- velocity 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
        </dependency>
    
        <!-- SpringBoot 相關依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  4. 配置 application.xml

    配置資料來源

    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切換成 Druid 資料來源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己繫結
        #druid 資料來源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置監控統計攔截的filters,stat:監控統計、log4j:日誌記錄、wall:防禦sql注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  5. 編寫程式碼生成器

    根據步驟,先建立程式碼生成器物件。然後 全域性配置 => 配置資料來源 => 包的配置 => 策略配置

     @Test
        void generator(){
            // 構建一個程式碼生成器物件
            AutoGenerator autoGenerator = new AutoGenerator();
            // 1.全域性配置
            GlobalConfig globalConfig = new GlobalConfig();
            globalConfig
                    .setOutputDir(System.getProperty("user.dir") + "/src/main/java") // 程式碼自動生成後存在的相對專案的位置
                    .setAuthor("xp") //設定作者名
                    .setOpen(false)
                    .setFileOverride(false) // 是否覆蓋
                    .setServiceName("%sService") // 自定義Service類名,預設生成的 Service類名前面有 I 開頭
                    .setMapperName("%sMapper")
                    .setServiceImplName("%sServiceImpl")
                    .setControllerName("%sController")
                    .setIdType(IdType.AUTO) // 配置主鍵策略,IdType.AUTO 代表主鍵自增
                    .setSwagger2(true); // 開啟 Swagger2 註解
            autoGenerator.setGlobalConfig(globalConfig); // 將我們的全域性設定注入到程式碼生成器中
    
            // 2. 配置資料來源
            DataSourceConfig dataSourceConfig = new DataSourceConfig();
            dataSourceConfig
                    .setUrl("jdbc:mysql://localhost:3306/mybatisplus?useUnicode=true&useSSL=false&characterEncoding=utf8")
                    .setDriverName("com.mysql.jdbc.Driver")
                    .setUsername("root")
                    .setPassword("root")
                    .setDbType(DbType.MYSQL);   // 設定資料庫型別為mysql
            autoGenerator.setDataSource(dataSourceConfig); // 將我們的資料來源注入到程式碼生成器中
    
            // 3. 包的配置
            PackageConfig packageConfig = new PackageConfig();
            packageConfig
    //                .setModuleName("test") // 設定模組名
                    .setController("controller")    // 設定 controller 的包
                    .setEntity("model.entity")      // 設定 實體類的包
                    .setService("service")          // 設定 Service 介面的包
                    .setServiceImpl("service.impl") // 設定 Service 介面實現類的包
                    .setMapper("mapper")            // 設定 mapper 的包
                    .setParent("com.xp");           // 設定這些包名的父包名
            autoGenerator.setPackageInfo(packageConfig); // 將我們包的配置注入到程式碼生成器中
    
            // 4. 策略配置
            StrategyConfig strategyConfig = new StrategyConfig();
            // 自動填充配置
            TableFill createTime = new TableFill("create_time", FieldFill.INSERT);
            TableFill modifiedTime = new TableFill("modified_time", FieldFill.INSERT_UPDATE);
            strategyConfig
                    .setInclude("user") // 設定要對映的表名
                    .setNaming(NamingStrategy.underline_to_camel) // 駝峰命名
                    .setColumnNaming(NamingStrategy.underline_to_camel) // 欄位名駝峰命名
                    .setLogicDeleteFieldName("deleted") // 自定義邏輯刪除欄位
                    .setEntityLombokModel(true)
                    .setEntityTableFieldAnnotationEnable(true)  // 實體類欄位生成註釋
                    .setVersionFieldName("version") // 樂觀鎖配置
                    .setTableFillList(Arrays.asList(createTime,modifiedTime))
                    .setControllerMappingHyphenStyle(true) // urlMapping駝峰轉字元
                    .setRestControllerStyle(true); // 生成的 Controller 加上 @RestController 註解
            autoGenerator.setStrategy(strategyConfig); // 將我們的策略配置注入到程式碼生成器中
    
            // 5. 執行程式碼生成器自動生成程式碼
            autoGenerator.execute();
        }
    
  6. 執行程式碼生成器

    執行程式碼生成器,然後我們會發現我們的專案多出了包和類,如下圖:

    點進去類裡,我們會發現它裡面已經幫我們全部寫好了

  7. 編寫 Controller 介面

    在我們自動生成的 UserController 類中增加一個hello 的介面

    /**
     * <p>
     *  前端控制器
     * </p>
     *
     * @author xp
     * @since 2020-08-01
     */
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        @Autowired
        UserService userService;
    
        @RequestMapping("/hello")
        public String hello(){
            return userService.getById(2).toString();
        }
    
    }
    
  8. 測試訪問介面

    我們通過瀏覽器訪問我們剛剛編寫好的介面,輸出我們資料庫中的資訊則代表程式碼生成器已經自動生成成功了