<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          真香!?MyBatis-Plus 從入門到上手干事!

          共 39852字,需瀏覽 80分鐘

           ·

          2021-05-19 23:57

          MyBatis 是一款優(yōu)秀的持久層框架,它支持定制化 SQL、存儲(chǔ)過程以及高級(jí)映射,而實(shí)際開發(fā)中,我們都會(huì)選擇使用 MyBatisPlus,它是對(duì) MyBatis 框架的進(jìn)一步增強(qiáng),能夠極大地簡(jiǎn)化我們的持久層代碼,下面就一起來看看 MyBatisPlus 中的一些奇淫巧技吧。

          說明:本篇文章需要一定的 MyBatisMyBatisPlus 基礎(chǔ)

          MyBatis-Plus 官網(wǎng)地址 :https://baomidou.com/

          本文已經(jīng)收錄進(jìn):https://github.com/CodingDocs/springboot-guide (SpringBoot2.0+從入門到實(shí)戰(zhàn)!)

          CRUD

          使用 MyBatisPlus 實(shí)現(xiàn)業(yè)務(wù)的增刪改查非常地簡(jiǎn)單,一起來看看吧。

          1.首先新建一個(gè) SpringBoot 工程,然后引入依賴:

          <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
          </dependency>
          <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
          </dependency>
          <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
          </dependency>

          2.配置一下數(shù)據(jù)源:

          spring:
            datasource:
              driver-class-name: com.mysql.cj.jdbc.Driver
              username: root
              url: jdbc:mysql:///mybatisplus?serverTimezone=UTC
              password: 123456

          3.創(chuàng)建一下數(shù)據(jù)表:

          CREATE DATABASE `mybatisplus`;

          USE `mybatisplus`;

          DROP TABLE IF EXISTS `tbl_employee`;

          CREATE TABLE `tbl_employee` (
            `id` bigint(20NOT NULL,
            `last_name` varchar(255DEFAULT NULL,
            `email` varchar(255DEFAULT NULL,
            `gender` char(1DEFAULT NULL,
            `age` int(11DEFAULT NULL,
            PRIMARY KEY (`id`)
          ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=gbk;

          insert  into `tbl_employee`(`id`,`last_name`,`email`,`gender`,`age`values (1,'jack','[email protected]','1',35),(2,'tom','[email protected]','1',30),(3,'jerry','[email protected]','1',40);

          4.創(chuàng)建對(duì)應(yīng)的實(shí)體類:

          @Data
          public class Employee {

              private Long id;
              private String lastName;
              private String email;
              private Integer age;
          }

          4.編寫 Mapper 接口:

          public interface EmployeeMapper extends BaseMapper<Employee{
          }

          我們只需繼承 MyBatisPlus 提供的 BaseMapper 接口即可,現(xiàn)在我們就擁有了對(duì) Employee 進(jìn)行增刪改查的 API,比如:

          @SpringBootTest
          @MapperScan("com.wwj.mybatisplusdemo.mapper")
          class MybatisplusDemoApplicationTests {

              @Autowired
              private EmployeeMapper employeeMapper;

              @Test
              void contextLoads() {
                  List<Employee> employees = employeeMapper.selectList(null);
                  employees.forEach(System.out::println);
              }
          }

          運(yùn)行結(jié)果:

          org.springframework.jdbc.BadSqlGrammarException:
          ### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: Table 'mybatisplus.employee' doesn't exist

          程序報(bào)錯(cuò)了,原因是不存在 employee 表,這是因?yàn)槲覀兊膶?shí)體類名為 EmployeeMyBatisPlus 默認(rèn)是以類名作為表名進(jìn)行操作的,可如果類名和表名不相同(實(shí)際開發(fā)中也確實(shí)可能不同),就需要在實(shí)體類中使用 @TableName 注解來聲明表的名稱:

          @Data
          @TableName("tbl_employee"// 聲明表名稱
          public class Employee {

              private Long id;
              private String lastName;
              private String email;
              private Integer age;
          }

          重新執(zhí)行測(cè)試代碼,結(jié)果如下:

          Employee(id=1, lastName=jack, email=jack@qq.com, age=35)
          Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
          Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40)

          BaseMapper 提供了常用的一些增刪改查方法:

          具體細(xì)節(jié)可以查閱其源碼自行體會(huì),注釋都是中文的,非常容易理解。

          在開發(fā)過程中,我們通常會(huì)使用 Service 層來調(diào)用 Mapper 層的方法,而 MyBatisPlus 也為我們提供了通用的 Service

          public interface EmployeeService extends IService<Employee{
          }

          @Service
          public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapperEmployeeimplements EmployeeService {
          }

          事實(shí)上,我們只需讓 EmployeeServiceImpl 繼承 ServiceImpl 即可獲得 Service 層的方法,那么為什么還需要實(shí)現(xiàn) EmployeeService 接口呢?

          這是因?yàn)閷?shí)現(xiàn) EmployeeService 接口能夠更方便地對(duì)業(yè)務(wù)進(jìn)行擴(kuò)展,一些復(fù)雜場(chǎng)景下的數(shù)據(jù)處理,MyBatisPlus 提供的 Service 方法可能無法處理,此時(shí)我們就需要自己編寫代碼,這時(shí)候只需在 EmployeeService 中定義自己的方法,并在 EmployeeServiceImpl 中實(shí)現(xiàn)即可。

          先來測(cè)試一下 MyBatisPlus 提供的 Service 方法:

          @SpringBootTest
          @MapperScan("com.wwj.mybatisplusdemo.mapper")
          class MybatisplusDemoApplicationTests {

              @Autowired
              private EmployeeService employeeService;

              @Test
              void contextLoads() {
                  List<Employee> list = employeeService.list();
                  list.forEach(System.out::println);
              }
          }

          運(yùn)行結(jié)果:

          Employee(id=1, lastName=jack, email=jack@qq.com, age=35)
          Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
          Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40)

          接下來模擬一個(gè)自定義的場(chǎng)景,我們來編寫自定義的操作方法,首先在 EmployeeMapper 中進(jìn)行聲明:

          public interface EmployeeMapper extends BaseMapper<Employee{

              List<Employee> selectAllByLastName(@Param("lastName") String lastName);
          }

          此時(shí)我們需要自己編寫配置文件實(shí)現(xiàn)該方法,在 resource 目錄下新建一個(gè) mapper 文件夾,然后在該文件夾下創(chuàng)建 EmployeeMapper.xml 文件:

          <!DOCTYPE mapper
                  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


          <mapper namespace="com.wwj.mybatisplusdemo.mapper.EmployeeMapper">

              <sql id="Base_Column">
                  id, last_name, email, gender, age
              </sql>

              <select id="selectAllByLastName" resultType="com.wwj.mybatisplusdemo.bean.Employee">
                  select <include refid="Base_Column"/>
                  from tbl_employee
                  where last_name = #{lastName}
              </select>
          </mapper>

          MyBatisPlus 默認(rèn)掃描的是類路徑下的 mapper 目錄,這可以從源碼中得到體現(xiàn):

          所以我們直接將 Mapper 配置文件放在該目錄下就沒有任何問題,可如果不是這個(gè)目錄,我們就需要進(jìn)行配置,比如:

          mybatis-plus:
            mapper-locations: classpath:xml/*.xml

          編寫好 Mapper 接口后,我們就需要定義 Service 方法了:

          public interface EmployeeService extends IService<Employee{

              List<Employee> listAllByLastName(String lastName);
          }

          @Service
          public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapperEmployeeimplements EmployeeService {

              @Override
              public List<Employee> listAllByLastName(String lastName) {
                  return baseMapper.selectAllByLastName(lastName);
              }
          }

          EmployeeServiceImpl 中我們無需將 EmployeeMapper 注入進(jìn)來,而是使用 BaseMapper,查看 ServiceImpl 的源碼:

          可以看到它為我們注入了一個(gè) BaseMapper 對(duì)象,而它是第一個(gè)泛型類型,也就是 EmployeeMapper 類型,所以我們可以直接使用這個(gè) baseMapper 來調(diào)用 Mapper 中的方法,此時(shí)編寫測(cè)試代碼:

          @SpringBootTest
          @MapperScan("com.wwj.mybatisplusdemo.mapper")
          class MybatisplusDemoApplicationTests {

              @Autowired
              private EmployeeService employeeService;

              @Test
              void contextLoads() {
                  List<Employee> list = employeeService.listAllByLastName("tom");
                  list.forEach(System.out::println);
              }
          }

          運(yùn)行結(jié)果:

          Employee(id=2, lastName=tom, email=tom@qq.com, age=30)

          ID 策略

          在創(chuàng)建表的時(shí)候我故意沒有設(shè)置主鍵的增長(zhǎng)策略,現(xiàn)在我們來插入一條數(shù)據(jù),看看主鍵是如何增長(zhǎng)的:

          @Test
          void contextLoads() {
              Employee employee = new Employee();
              employee.setLastName("lisa");
              employee.setEmail("[email protected]");
              employee.setAge(20);
              employeeService.save(employee);
          }

          插入成功后查詢一下數(shù)據(jù)表:

          mysql> select * from tbl_employee;
          +---------------------+-----------+--------------+--------+------+
          | id                  | last_name | email        | gender | age  |
          +---------------------+-----------+--------------+--------+------+
          |                   1 | jack      | [email protected]  | 1      |   35 |
          |                   2 | tom       | [email protected]   | 1      |   30 |
          |                   3 | jerry     | [email protected] | 1      |   40 |
          | 1385934720849584129 | lisa      | [email protected]  | NULL   |   20 |
          +---------------------+-----------+--------------+--------+------+
          4 rows in set (0.00 sec)

          可以看到 id 是一串相當(dāng)長(zhǎng)的數(shù)字,這是什么意思呢?提前劇透一下,這其實(shí)是分布式 id,那又何為分布式 id 呢?

          我們知道,對(duì)于一個(gè)大型應(yīng)用,其訪問量是非常巨大的,就比如說一個(gè)網(wǎng)站每天都有人進(jìn)行注冊(cè),注冊(cè)的用戶信息就需要存入數(shù)據(jù)表,隨著日子一天天過去,數(shù)據(jù)表中的用戶越來越多,此時(shí)數(shù)據(jù)庫的查詢速度就會(huì)受到影響,所以一般情況下,當(dāng)數(shù)據(jù)量足夠龐大時(shí),數(shù)據(jù)都會(huì)做分庫分表的處理。

          然而,一旦分表,問題就產(chǎn)生了,很顯然這些分表的數(shù)據(jù)都是屬于同一張表的數(shù)據(jù),只是因?yàn)閿?shù)據(jù)量過大而分成若干張表,那么這幾張表的主鍵 id 該怎么管理呢?每張表維護(hù)自己的 id?那數(shù)據(jù)將會(huì)有很多的 id 重復(fù),這當(dāng)然是不被允許的,其實(shí),我們可以使用算法來生成一個(gè)絕對(duì)不會(huì)重復(fù)的 id,這樣問題就迎刃而解了,事實(shí)上,分布式 id 的解決方案有很多:

          1. UUID
          2. SnowFlake
          3. TinyID
          4. Uidgenerator
          5. Leaf
          6. Tinyid
          7. ......

          以 UUID 為例,它生成的是一串由數(shù)字和字母組成的字符串,顯然并不適合作為數(shù)據(jù)表的 id,而且 id 保持遞增有序會(huì)加快表的查詢效率,基于此,MyBatisPlus 使用的就是 SnowFlake(雪花算法)。

          Snowflake 是 Twitter 開源的分布式 ID 生成算法。Snowflake 由 64 bit 的二進(jìn)制數(shù)字組成,這 64bit 的二進(jìn)制被分成了幾部分,每一部分存儲(chǔ)的數(shù)據(jù)都有特定的含義:

          • 第 0 位:符號(hào)位(標(biāo)識(shí)正負(fù)),始終為 0,沒有用,不用管。
          • 第 1~41 位 :一共 41 位,用來表示時(shí)間戳,單位是毫秒,可以支撐 2 ^41 毫秒(約 69 年)
          • 第 42~52 位 :一共 10 位,一般來說,前 5 位表示機(jī)房 ID,后 5 位表示機(jī)器 ID(實(shí)際項(xiàng)目中可以根據(jù)實(shí)際情況調(diào)整)。這樣就可以區(qū)分不同集群/機(jī)房的節(jié)點(diǎn)。
          • 第 53~64 位 :一共 12 位,用來表示序列號(hào)。序列號(hào)為自增值,代表單臺(tái)機(jī)器每毫秒能夠產(chǎn)生的最大 ID 數(shù)(2^12 = 4096),也就是說單臺(tái)機(jī)器每毫秒最多可以生成 4096 個(gè) 唯一 ID。

          這也就是為什么插入數(shù)據(jù)后新的數(shù)據(jù) id 是一長(zhǎng)串?dāng)?shù)字的原因了,我們可以在實(shí)體類中使用 @TableId 來設(shè)置主鍵的策略:

          @Data
          @TableName("tbl_employee")
          public class Employee {

              @TableId(type = IdType.AUTO) // 設(shè)置主鍵策略
              private Long id;
              private String lastName;
              private String email;
              private Integer age;
          }

          MyBatisPlus 提供了幾種主鍵的策略:其中 AUTO 表示數(shù)據(jù)庫自增策略,該策略下需要數(shù)據(jù)庫實(shí)現(xiàn)主鍵的自增(auto_increment),ASSIGN_ID 是雪花算法,默認(rèn)使用的是該策略,ASSIGN_UUID 是 UUID 策略,一般不會(huì)使用該策略。

          這里多說一點(diǎn), 當(dāng)實(shí)體類的主鍵名為 id,并且數(shù)據(jù)表的主鍵名也為 id 時(shí),此時(shí) MyBatisPlus 會(huì)自動(dòng)判定該屬性為主鍵 id,倘若名字不是 id 時(shí),就需要標(biāo)注 @TableId 注解,若是實(shí)體類中主鍵名與數(shù)據(jù)表的主鍵名不一致,則可以進(jìn)行聲明:

          @TableId(value = "uid",type = IdType.AUTO) // 設(shè)置主鍵策略
          private Long id;

          還可以在配置文件中配置全局的主鍵策略:

          mybatis-plus:
            global-config:
              db-config:
                id-type: auto

          這樣能夠避免在每個(gè)實(shí)體類中重復(fù)設(shè)置主鍵策略。

          屬性自動(dòng)填充

          翻閱《阿里巴巴 Java 開發(fā)手冊(cè)》,在第 5 章 MySQL 數(shù)據(jù)庫可以看到這樣一條規(guī)范:對(duì)于一張數(shù)據(jù)表,它必須具備三個(gè)字段:

          • id : 唯一 ID
          • gmt_create : 保存的是當(dāng)前數(shù)據(jù)創(chuàng)建的時(shí)間
          • gmt_modified : 保存的是更新時(shí)間

          我們改造一下數(shù)據(jù)表:

          alter table tbl_employee add column gmt_create datetime not null;
          alter table tbl_employee add column gmt_modified datetime not null;

          然后改造一下實(shí)體類:

          @Data
          @TableName("tbl_employee")
          public class Employee {

              @TableId(type = IdType.AUTO) // 設(shè)置主鍵策略
              private Long id;
              private String lastName;
              private String email;
              private Integer age;
              private LocalDateTime gmtCreate;
              private LocalDateTime gmtModified;
          }

          此時(shí)我們?cè)诓迦霐?shù)據(jù)和更新數(shù)據(jù)的時(shí)候就需要手動(dòng)去維護(hù)這兩個(gè)屬性:

          @Test
          void contextLoads() {
              Employee employee = new Employee();
              employee.setLastName("lisa");
              employee.setEmail("[email protected]");
              employee.setAge(20);
              // 設(shè)置創(chuàng)建時(shí)間
              employee.setGmtCreate(LocalDateTime.now());
              employee.setGmtModified(LocalDateTime.now());
              employeeService.save(employee);
          }

          @Test
          void contextLoads() {
              Employee employee = new Employee();
              employee.setId(1385934720849584130L);
              employee.setAge(50);
              // 設(shè)置創(chuàng)建時(shí)間
              employee.setGmtModified(LocalDateTime.now());
              employeeService.updateById(employee);
          }

          每次都需要維護(hù)這兩個(gè)屬性未免過于麻煩,好在 MyBatisPlus 提供了字段自動(dòng)填充功能來幫助我們進(jìn)行管理,需要使用到的是 @TableField 注解:

          @Data
          @TableName("tbl_employee")
          public class Employee {

              @TableId(type = IdType.AUTO)
              private Long id;
              private String lastName;
              private String email;
              private Integer age;
              @TableField(fill = FieldFill.INSERT) // 插入的時(shí)候自動(dòng)填充
              private LocalDateTime gmtCreate;
              @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新的時(shí)候自動(dòng)填充
              private LocalDateTime gmtModified;
          }

          然后編寫一個(gè)類實(shí)現(xiàn) MetaObjectHandler 接口:

          @Component
          @Slf4j
          public class MyMetaObjectHandler implements MetaObjectHandler {

              /**
               * 實(shí)現(xiàn)插入時(shí)的自動(dòng)填充
               * @param metaObject
               */

              @Override
              public void insertFill(MetaObject metaObject) {
                  log.info("insert開始屬性填充");
                  this.strictInsertFill(metaObject,"gmtCreate", LocalDateTime.class,LocalDateTime.now());
                  this.strictInsertFill(metaObject,"gmtModified", LocalDateTime.class,LocalDateTime.now());
              }

              /**
               * 實(shí)現(xiàn)更新時(shí)的自動(dòng)填充
               * @param metaObject
               */

              @Override
              public void updateFill(MetaObject metaObject) {
                  log.info("update開始屬性填充");
                  this.strictInsertFill(metaObject,"gmtModified", LocalDateTime.class,LocalDateTime.now());
              }
          }

          該接口中有兩個(gè)未實(shí)現(xiàn)的方法,分別為插入和更新時(shí)的填充方法,在方法中調(diào)用 strictInsertFill() 方法 即可實(shí)現(xiàn)屬性的填充,它需要四個(gè)參數(shù):

          1. metaObject:元對(duì)象,就是方法的入?yún)?/section>
          2. fieldName:為哪個(gè)屬性進(jìn)行自動(dòng)填充
          3. fieldType:屬性的類型
          4. fieldVal:需要填充的屬性值

          此時(shí)在插入和更新數(shù)據(jù)之前,這兩個(gè)方法會(huì)先被執(zhí)行,以實(shí)現(xiàn)屬性的自動(dòng)填充,通過日志我們可以進(jìn)行驗(yàn)證:

          @Test
          void contextLoads() {
              Employee employee = new Employee();
              employee.setId(1385934720849584130L);
              employee.setAge(15);
              employeeService.updateById(employee);
          }

          運(yùn)行結(jié)果:

          INFO 15584 --- [           main] c.w.m.handler.MyMetaObjectHandler        : update開始屬性填充
          2021-04-24 21:32:19.788  INFO 15584 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
          2021-04-24 21:32:21.244  INFO 15584 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.

          屬性填充其實(shí)可以進(jìn)行一些優(yōu)化,考慮一些特殊情況,對(duì)于一些不存在的屬性,就不需要進(jìn)行屬性填充,對(duì)于一些設(shè)置了值的屬性,也不需要進(jìn)行屬性填充,這樣可以提高程序的整體運(yùn)行效率:

          @Component
          @Slf4j
          public class MyMetaObjectHandler implements MetaObjectHandler {

              @Override
              public void insertFill(MetaObject metaObject) {
                  boolean hasGmtCreate = metaObject.hasSetter("gmtCreate");
                  boolean hasGmtModified = metaObject.hasSetter("gmtModified");
                  if (hasGmtCreate) {
                      Object gmtCreate = this.getFieldValByName("gmtCreate", metaObject);
                      if (gmtCreate == null) {
                          this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.classLocalDateTime.now());
                      }
                  }
                  if (hasGmtModified) {
                      Object gmtModified = this.getFieldValByName("gmtModified", metaObject);
                      if (gmtModified == null) {
                          this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.classLocalDateTime.now());
                      }
                  }
              }

              @Override
              public void updateFill(MetaObject metaObject) {
                  boolean hasGmtModified = metaObject.hasSetter("gmtModified");
                  if (hasGmtModified) {
                      Object gmtModified = this.getFieldValByName("gmtModified", metaObject);
                      if (gmtModified == null) {
                          this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.classLocalDateTime.now());
                      }
                  }
              }
          }

          邏輯刪除

          邏輯刪除對(duì)應(yīng)的是物理刪除,分別介紹一下這兩個(gè)概念:

          1. 物理刪除 :指的是真正的刪除,即:當(dāng)執(zhí)行刪除操作時(shí),將數(shù)據(jù)表中的數(shù)據(jù)進(jìn)行刪除,之后將無法再查詢到該數(shù)據(jù)
          2. 邏輯刪除 :并不是真正意義上的刪除,只是對(duì)于用戶不可見了,它仍然存在與數(shù)據(jù)表中

          在這個(gè)數(shù)據(jù)為王的時(shí)代,數(shù)據(jù)就是財(cái)富,所以一般并不會(huì)有哪個(gè)系統(tǒng)在刪除某些重要數(shù)據(jù)時(shí)真正刪掉了數(shù)據(jù),通常都是在數(shù)據(jù)庫中建立一個(gè)狀態(tài)列,讓其默認(rèn)為 0,當(dāng)為 0 時(shí),用戶可見;當(dāng)執(zhí)行了刪除操作,就將狀態(tài)列改為 1,此時(shí)用戶不可見,但數(shù)據(jù)還是在表中的。

          按照《阿里巴巴 Java 開發(fā)手冊(cè)》第 5 章 MySQL 數(shù)據(jù)庫相關(guān)的建議,我們來為數(shù)據(jù)表新增一個(gè)is_deleted 字段:

          alter table tbl_employee add column is_deleted tinyint not null;

          在實(shí)體類中也要添加這一屬性:

          @Data
          @TableName("tbl_employee")
          public class Employee {

              @TableId(type = IdType.AUTO)
              private Long id;
              private String lastName;
              private String email;
              private Integer age;
              @TableField(fill = FieldFill.INSERT)
              private LocalDateTime gmtCreate;
              @TableField(fill = FieldFill.INSERT_UPDATE)
              private LocalDateTime gmtModified;
              /**
               * 邏輯刪除屬性
               */

              @TableLogic
              @TableField("is_deleted")
              private Boolean deleted;
          }

          還是參照《阿里巴巴 Java 開發(fā)手冊(cè)》第 5 章 MySQL 數(shù)據(jù)庫相關(guān)的建議,對(duì)于布爾類型變量,不能加 is 前綴,所以我們的屬性被命名為 deleted,但此時(shí)就無法與數(shù)據(jù)表的字段進(jìn)行對(duì)應(yīng)了,所以我們需要使用 @TableField 注解來聲明一下數(shù)據(jù)表的字段名,而 @TableLogin 注解用于設(shè)置邏輯刪除屬性;此時(shí)我們執(zhí)行刪除操作:

          @Test
          void contextLoads() {
              employeeService.removeById(3);
          }

          查詢數(shù)據(jù)表:

          mysql> select * from tbl_employee;
          +---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+
          | id                  | last_name | email        | gender | age  | gmt_create          | gmt_modified        | is_deleted |
          +---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+
          |                   1 | jack      | [email protected]  | 1      |   35 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 |          0 |
          |                   2 | tom       | [email protected]   | 1      |   30 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 |          0 |
          |                   3 | jerry     | [email protected] | 1      |   40 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 |          1 |
          | 1385934720849584129 | lisa      | [email protected]  | NULL   |   20 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 |          0 |
          | 1385934720849584130 | lisa      | [email protected]  | NULL   |   15 | 2021-04-24 21:14:18 | 2021-04-24 21:32:19 |          0 |
          +---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+
          5 rows in set (0.00 sec)

          可以看到數(shù)據(jù)并沒有被刪除,只是 is_deleted 字段的屬性值被更新成了 1,此時(shí)我們?cè)賮韴?zhí)行查詢操作:

          @Test
          void contextLoads() {
              List<Employee> list = employeeService.list();
              list.forEach(System.out::println);
          }

          執(zhí)行結(jié)果:

          Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
          Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
          Employee(id=1385934720849584129, lastName=lisa, email=lisa@qq.com, age=20, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
          Employee(id=1385934720849584130, lastName=lisa, email=lisa@qq.com, age=15, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:32:19, deleted=false)

          會(huì)發(fā)現(xiàn)第三條數(shù)據(jù)并沒有被查詢出來,它是如何實(shí)現(xiàn)的呢?我們可以輸出 MyBatisPlus 生成的 SQL 來分析一下,在配置文件中進(jìn)行配置:

          mybatis-plus:
            configuration:
              log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 輸出SQL日志

          運(yùn)行結(jié)果:

          ==>  Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0
          ==> Parameters:
          <==    Columns: id, last_name, email, age, gmt_create, gmt_modified, deleted
          <==        Row: 1, jack, jack@qq.com, 352021-04-24 21:14:182021-04-24 21:14:180
          <==        Row: 2, tom, tom@qq.com, 302021-04-24 21:14:182021-04-24 21:14:180
          <==        Row: 1385934720849584129, lisa, lisa@qq.com, 202021-04-24 21:14:182021-04-24 21:14:180
          <==        Row: 1385934720849584130, lisa, lisa@qq.com, 152021-04-24 21:14:182021-04-24 21:32:190
          <==      Total: 4

          原來它在查詢時(shí)攜帶了一個(gè)條件:is_deleted=0 ,這也說明了 MyBatisPlus 默認(rèn) 0 為不刪除,1 為刪除。若是你想修改這個(gè)規(guī)定,比如設(shè)置-1 為刪除,1 為不刪除,也可以進(jìn)行配置:

          mybatis-plus:
            global-config:
              db-config:
                id-type: auto
                logic-delete-field: deleted # 邏輯刪除屬性名
                logic-delete-value: -1 # 刪除值
                logic-not-delete-value: 1 # 不刪除值

          但建議使用默認(rèn)的配置,阿里巴巴開發(fā)手冊(cè)也規(guī)定 1 表示刪除,0 表示未刪除。

          分頁插件

          對(duì)于分頁功能,MyBatisPlus 提供了分頁插件,只需要進(jìn)行簡(jiǎn)單的配置即可實(shí)現(xiàn):

          @Configuration
          public class MyBatisConfig {

              /**
               * 注冊(cè)分頁插件
               * @return
               */

              @Bean
              public MybatisPlusInterceptor mybatisPlusInterceptor() {
                  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
                  interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
                  return interceptor;
              }
          }

          接下來我們就可以使用分頁插件提供的功能了:

          @Test
          void contextLoads() {
              Page<Employee> page = new Page<>(1,2);
              employeeService.page(page, null);
              List<Employee> employeeList = page.getRecords();
              employeeList.forEach(System.out::println);
              System.out.println("獲取總條數(shù):" + page.getTotal());
              System.out.println("獲取當(dāng)前頁碼:" + page.getCurrent());
              System.out.println("獲取總頁碼:" + page.getPages());
              System.out.println("獲取每頁顯示的數(shù)據(jù)條數(shù):" + page.getSize());
              System.out.println("是否有上一頁:" + page.hasPrevious());
              System.out.println("是否有下一頁:" + page.hasNext());
          }

          其中的 Page 對(duì)象用于指定分頁查詢的規(guī)則,這里表示按每頁兩條數(shù)據(jù)進(jìn)行分頁,并查詢第一頁的內(nèi)容,運(yùn)行結(jié)果:

          Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
          Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
          獲取總條數(shù):4
          獲取當(dāng)前頁碼:1
          獲取總頁碼:2
          獲取每頁顯示的數(shù)據(jù)條數(shù):2
          是否有上一頁:false
          是否有下一頁:true

          倘若在分頁過程中需要限定一些條件,我們就需要構(gòu)建 QueryWrapper 來實(shí)現(xiàn):

          @Test
          void contextLoads() {
              Page<Employee> page = new Page<>(12);
              employeeService.page(page, new QueryWrapper<Employee>()
                                   .between("age"2050)
                                   .eq("gender"1));
              List<Employee> employeeList = page.getRecords();
              employeeList.forEach(System.out::println);
          }

          此時(shí)分頁的數(shù)據(jù)就應(yīng)該是年齡在 20~50 歲之間,且 gender 值為 1 的員工信息,然后再對(duì)這些數(shù)據(jù)進(jìn)行分頁。

          樂觀鎖

          當(dāng)程序中出現(xiàn)并發(fā)訪問時(shí),就需要保證數(shù)據(jù)的一致性。以商品系統(tǒng)為例,現(xiàn)在有兩個(gè)管理員均想對(duì)同一件售價(jià)為 100 元的商品進(jìn)行修改,A 管理員正準(zhǔn)備將商品售價(jià)改為 150 元,但此時(shí)出現(xiàn)了網(wǎng)絡(luò)問題,導(dǎo)致 A 管理員的操作陷入了等待狀態(tài);此時(shí) B 管理員也進(jìn)行修改,將商品售價(jià)改為了 200 元,修改完成后 B 管理員退出了系統(tǒng),此時(shí) A 管理員的操作也生效了,這樣便使得 A 管理員的操作直接覆蓋了 B 管理員的操作,B 管理員后續(xù)再進(jìn)行查詢時(shí)會(huì)發(fā)現(xiàn)商品售價(jià)變?yōu)榱?150 元,這樣的情況是絕對(duì)不允許發(fā)生的。

          要想解決這一問題,可以給數(shù)據(jù)表加鎖,常見的方式有兩種:

          1. 樂觀鎖
          2. 悲觀鎖

          悲觀鎖認(rèn)為并發(fā)情況一定會(huì)發(fā)生,所以在某條數(shù)據(jù)被修改時(shí),為了避免其它人修改,會(huì)直接對(duì)數(shù)據(jù)表進(jìn)行加鎖,它依靠的是數(shù)據(jù)庫本身提供的鎖機(jī)制(表鎖、行鎖、讀鎖、寫鎖)。

          而樂觀鎖則相反,它認(rèn)為數(shù)據(jù)產(chǎn)生沖突的情況一般不會(huì)發(fā)生,所以在修改數(shù)據(jù)的時(shí)候并不會(huì)對(duì)數(shù)據(jù)表進(jìn)行加鎖的操作,而是在提交數(shù)據(jù)時(shí)進(jìn)行校驗(yàn),判斷提交上來的數(shù)據(jù)是否會(huì)發(fā)生沖突,如果發(fā)生沖突,則提示用戶重新進(jìn)行操作,一般的實(shí)現(xiàn)方式為 設(shè)置版本號(hào)字段

          就以商品售價(jià)為例,在該表中設(shè)置一個(gè)版本號(hào)字段,讓其初始為 1,此時(shí) A 管理員和 B 管理員同時(shí)需要修改售價(jià),它們會(huì)先讀取到數(shù)據(jù)表中的內(nèi)容,此時(shí)兩個(gè)管理員讀取到的版本號(hào)都為 1,此時(shí) B 管理員的操作先生效了,它就會(huì)將當(dāng)前數(shù)據(jù)表中對(duì)應(yīng)數(shù)據(jù)的版本號(hào)與最開始讀取到的版本號(hào)作一個(gè)比對(duì),發(fā)現(xiàn)沒有變化,于是修改就生效了,此時(shí)版本號(hào)加 1。

          而 A 管理員馬上也提交了修改操作,但是此時(shí)的版本號(hào)為 2,與最開始讀取到的版本號(hào)并不對(duì)應(yīng),這就說明數(shù)據(jù)發(fā)生了沖突,此時(shí)應(yīng)該提示 A 管理員操作失敗,并讓 A 管理員重新查詢一次數(shù)據(jù)。

          樂觀鎖的優(yōu)勢(shì)在于采取了更加寬松的加鎖機(jī)制,能夠提高程序的吞吐量,適用于讀操作多的場(chǎng)景。

          那么接下來我們就來模擬這一過程。

          1.創(chuàng)建一張新的數(shù)據(jù)表:

          create table shop(
           id bigint(20not null auto_increment,
            name varchar(30not null,
            price int(11default 0,
            version int(11default 1,
            primary key(id)
          );

          insert into shop(id,name,price) values(1,'筆記本電腦',8000);

          2.創(chuàng)建實(shí)體類:

          @Data
          public class Shop {

              private Long id;
              private String name;
              private Integer price;
              private Integer version;
          }

          3.創(chuàng)建對(duì)應(yīng)的 Mapper 接口:

          public interface ShopMapper extends BaseMapper<Shop{
          }

          4.編寫測(cè)試代碼:

          @SpringBootTest
          @MapperScan("com.wwj.mybatisplusdemo.mapper")
          class MybatisplusDemoApplicationTests {

              @Autowired
              private ShopMapper shopMapper;

              /**
               * 模擬并發(fā)場(chǎng)景
               */

              @Test
              void contextLoads() {
                  // A、B管理員讀取數(shù)據(jù)
                  Shop A = shopMapper.selectById(1L);
                  Shop B = shopMapper.selectById(1L);
                  // B管理員先修改
                  B.setPrice(9000);
                  int result = shopMapper.updateById(B);
                  if (result == 1) {
                      System.out.println("B管理員修改成功!");
                  } else {
                      System.out.println("B管理員修改失敗!");
                  }
                  // A管理員后修改
                  A.setPrice(8500);
                  int result2 = shopMapper.updateById(A);
                  if (result2 == 1) {
                      System.out.println("A管理員修改成功!");
                  } else {
                      System.out.println("A管理員修改失敗!");
                  }
                  // 最后查詢
                  System.out.println(shopMapper.selectById(1L));
              }
          }

          執(zhí)行結(jié)果:

          B管理員修改成功!
          A管理員修改成功!
          Shop(id=1, name=筆記本電腦, price=8500, version=1)

          問題出現(xiàn)了,B 管理員的操作被 A 管理員覆蓋,那么該如何解決這一問題呢?

          其實(shí) MyBatisPlus 已經(jīng)提供了樂觀鎖機(jī)制,只需要在實(shí)體類中使用 @Version 聲明版本號(hào)屬性:

          @Data
          public class Shop {

              private Long id;
              private String name;
              private Integer price;
              @Version // 聲明版本號(hào)屬性
              private Integer version;
          }

          然后注冊(cè)樂觀鎖插件:

          @Configuration
          public class MyBatisConfig {

              /**
               * 注冊(cè)插件
               * @return
               */

              @Bean
              public MybatisPlusInterceptor mybatisPlusInterceptor() {
                  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
                  // 分頁插件
                  interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
                  // 樂觀鎖插件
                  interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
                  return interceptor;
              }
          }

          重新執(zhí)行測(cè)試代碼,結(jié)果如下:

          B管理員修改成功!
          A管理員修改失敗!
          Shop(id=1, name=筆記本電腦, price=9000, version=2)

          此時(shí) A 管理員的修改就失敗了,它需要重新讀取最新的數(shù)據(jù)才能再次進(jìn)行修改。

          條件構(gòu)造器

          在分頁插件中我們簡(jiǎn)單地使用了一下條件構(gòu)造器(Wrapper),下面我們來詳細(xì)了解一下。先來看看 Wrapper 的繼承體系:分別介紹一下它們的作用:

          • Wrapper:條件構(gòu)造器抽象類,最頂端的父類
            • LambdaQueryWrapper:用于對(duì)象封裝,使用 Lambda 語法
            • LambdaUpdateWrapper:用于條件封裝,使用 Lambda 語法
            • QueryWrapper:用于對(duì)象封裝
            • UpdateWrapper:用于條件封裝
            • AbstractWrapper:查詢條件封裝抽象類,生成 SQL 的 where 條件
            • AbstractLambdaWrapper:Lambda 語法使用 Wrapper

          通常我們使用的都是 QueryWrapperUpdateWrapper,若是想使用 Lambda 語法來編寫,也可以使用 LambdaQueryWrapperLambdaUpdateWrapper,通過這些條件構(gòu)造器,我們能夠很方便地來實(shí)現(xiàn)一些復(fù)雜的篩選操作,比如:

          @SpringBootTest
          @MapperScan("com.wwj.mybatisplusdemo.mapper")
          class MybatisplusDemoApplicationTests {

              @Autowired
              private EmployeeMapper employeeMapper;

              @Test
              void contextLoads() {
                  // 查詢名字中包含'j',年齡大于20歲,郵箱不為空的員工信息
                  QueryWrapper<Employee> wrapper = new QueryWrapper<>();
                  wrapper.like("last_name""j");
                  wrapper.gt("age"20);
                  wrapper.isNotNull("email");
                  List<Employee> list = employeeMapper.selectList(wrapper);
                  list.forEach(System.out::println);
              }
          }

          運(yùn)行結(jié)果:

          Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)

          條件構(gòu)造器提供了豐富的條件方法幫助我們進(jìn)行條件的構(gòu)造,比如 like 方法會(huì)為我們建立模糊查詢,查看一下控制臺(tái)輸出的 SQL:

          ==>  Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0 AND (last_name LIKE ? AND age > ? AND email IS NOT NULL)
          ==> Parameters: %j%(String), 20(Integer)

          可以看到它是對(duì) j 的前后都加上了 % ,若是只想查詢以 j 開頭的名字,則可以使用 likeRight 方法,若是想查詢以 j 結(jié)尾的名字,,則使用 likeLeft 方法。

          年齡的比較也是如此, gt 是大于指定值,若是小于則調(diào)用 lt ,大于等于調(diào)用 ge ,小于等于調(diào)用 le ,不等于調(diào)用 ne ,還可以使用 between 方法實(shí)現(xiàn)這一過程,相關(guān)的其它方法都可以查閱源碼進(jìn)行學(xué)習(xí)。

          因?yàn)檫@些方法返回的其實(shí)都是自身實(shí)例,所以可使用鏈?zhǔn)骄幊蹋?/p>

          @Test
          void contextLoads() {
              // 查詢名字中包含'j',年齡大于20歲,郵箱不為空的員工信息
              QueryWrapper<Employee> wrapper = new QueryWrapper<Employee>()
                  .likeLeft("last_name""j")
                  .gt("age"20)
                  .isNotNull("email");
              List<Employee> list = employeeMapper.selectList(wrapper);
              list.forEach(System.out::println);
          }

          也可以使用 LambdaQueryWrapper 實(shí)現(xiàn):

          @Test
          void contextLoads() {
              // 查詢名字中包含'j',年齡大于20歲,郵箱不為空的員工信息
              LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<Employee>()
                  .like(Employee::getLastName,"j")
                  .gt(Employee::getAge,20)
                  .isNotNull(Employee::getEmail);
              List<Employee> list = employeeMapper.selectList(wrapper);
              list.forEach(System.out::println);
          }

          這種方式的好處在于對(duì)字段的設(shè)置不是硬編碼,而是采用方法引用的形式,效果與 QueryWrapper 是一樣的。

          UpdateWrapperQueryWrapper 不同,它的作用是封裝更新內(nèi)容的,比如:

          @Test
          void contextLoads() {
              UpdateWrapper<Employee> wrapper = new UpdateWrapper<Employee>()
                  .set("age"50)
                  .set("email""[email protected]")
                  .like("last_name""j")
                  .gt("age"20);
              employeeMapper.update(null, wrapper);
          }

          將名字中包含 j 且年齡大于 20 歲的員工年齡改為 50,郵箱改為 [email protected]UpdateWrapper 不僅能夠封裝更新內(nèi)容,也能作為查詢條件,所以在更新數(shù)據(jù)時(shí)可以直接構(gòu)造一個(gè) UpdateWrapper 來設(shè)置更新內(nèi)容和條件。

          本文已經(jīng)收錄進(jìn):https://github.com/CodingDocs/springboot-guide (SpringBoot2.0+從入門到實(shí)戰(zhàn)!)


          < END >


          推薦?? :1049天,100K!簡(jiǎn)單復(fù)盤!

          推薦?? :年薪 40W Java 開發(fā)是什么水平?

          推薦?? :Github掘金計(jì)劃:Github上的一些優(yōu)質(zhì)項(xiàng)目搜羅

          我是 Guide哥,擁抱開源,喜歡烹飪。Github 接近 10w 點(diǎn)贊的開源項(xiàng)目 JavaGuide 的作者。未來幾年,希望持續(xù)完善 JavaGuide,爭(zhēng)取能夠幫助更多學(xué)習(xí) Java 的小伙伴!共勉!凎!點(diǎn)擊查看我的2020年工作匯報(bào)!
          歡迎準(zhǔn)備面試的朋友加入我的星球
          一個(gè)純 Java 面試交流圈子 !Ready!
          原創(chuàng)不易,歡迎點(diǎn)贊分享。咱們下期再會(huì)!
          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品九九 | 中文字幕在线网站 | 久久艹影院 | 日韩一区二区三区中文高清电影 | 国产综合在线视频网 |