1. 程式人生 > 其它 >執行緒----概念引入---執行緒和程序的區別

執行緒----概念引入---執行緒和程序的區別

目錄

Mybatis-plus

一、入門案例

1、開發環境

IDE:idea 2021.2

JDK:JDK8+

構建工具:maven 3.5.4

MySQL版本:MySQL 5.7

Spring Boot:2.6.7

MyBatis-Plus:3.5.1

2、建立資料庫及表

a>建立表

CREATE DATABASE `mybatis_plus` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; use `mybatis_plus`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT '主鍵ID',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年齡',
`email` varchar(50) DEFAULT NULL COMMENT '郵箱', PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用bigint的原因:mybatis-plus預設使用雪花演算法生成id,長度比較長。

b>新增資料

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]');

3、建立Spring Boot工程

a>初始化工程

使用 Spring Initializr 快速初始化一個 Spring Boot 工程

b>引入依賴

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

4、編寫程式碼

a>配置application.yml

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&userSSL=false
    username: root
    password: root

注意:

1、驅動類driver-class-name

spring boot 2.0(內建jdbc5驅動),驅動類使用:

driver-class-name: com.mysql.jdbc.Driver

spring boot 2.1及以上(內建jdbc8驅動),驅動類使用: driver-class-name: com.mysql.cj.jdbc.Driver

否則執行測試用例的時候會有 WARN 資訊

2、連線地址url MySQL5.7版本的url:

jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false

MySQL8.0版本的url: jdbc:mysql://localhost:3306/mybatis_plus?

serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false

否則執行測試用例報告如下錯誤:

java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more

b>啟動類

在Spring Boot啟動類中新增@MapperScan註解,掃描mapper包

@MapperScan("com.ekertree.mybatisplus.mapper")
@SpringBootApplication
public class MybatisPlusApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusApplication.class, args);
    }

}

c>新增實體

@Data
public class User {

    private Long id;

    private String name;

    private Integer age;

    private String email;
}

d>新增mapper

BaseMapper是MyBatis-Plus提供的模板mapper,其中包含了基本的CRUD方法,泛型為操作的實體型別

public interface UserMapper extends BaseMapper<User> {
}

e>測試

@SpringBootTest
public class MybatisPlusTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testSelectList(){
        List<User> users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }

}

結果:

注意:

IDEA在 userMapper 處報錯,因為找不到注入的物件,因為類是動態建立的,但是程式可以正確的執行。
為了避免報錯,可以在mapper介面上新增 @Repository 註解

f>新增日誌

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

二、基本CRUD

1、BaseMapper

MyBatis-Plus中的基本CRUD在內建的BaseMapper中都已得到了實現,我們可以直接使用,介面如下:

public interface BaseMapper<T> extends Mapper<T> {


/**
*插入一條記錄
*@param entity 實體物件
*/
int insert(T entity);


/**
*根據 ID 刪除
*@param id 主鍵ID
*/
int deleteById(Serializable id);


/**
*根據實體(ID)刪除
*@param entity 實體物件
* @since 3.4.4
*/

int deleteById(T entity);


/**
*根據 columnMap 條件,刪除記錄
*@param columnMap 表字段 map 物件
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);


/**
*根據 entity 條件,刪除記錄
*@param queryWrapper 實體物件封裝操作類(可以為 null,裡面的 entity 用於生成 where
語句)
*/
int delete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);


/**
*刪除(根據ID 批量刪除)
*@param idList 主鍵ID列表(不能為 null 以及 empty)
*/
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

/**
*根據 ID 修改
*@param entity 實體物件
*/
int updateById(@Param(Constants.ENTITY) T entity);


/**
*根據 whereEntity 條件,更新記錄
*@param entity	實體物件 (set 條件值,可以為 null)
*@param updateWrapper 實體物件封裝操作類(可以為 null,裡面的 entity 用於生成
where 語句)
*/
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);

/**
*根據 ID 查詢
*@param id 主鍵ID
*/
T selectById(Serializable id);


/**
*查詢(根據ID 批量查詢)
*@param idList 主鍵ID列表(不能為 null 以及 empty)
*/
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

/**
*查詢(根據 columnMap 條件)
*@param columnMap 表字段 map 物件
*/
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

/**
*根據 entity 條件,查詢一條記錄
*<p>查詢一條記錄,例如 qw.last("limit 1") 限制取一條記錄, 注意:多條資料會報異常

*@param queryWrapper 實體物件封裝操作類(可以為 null)
*/

default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) { List<T> ts = this.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(ts)) { if (ts.size() != 1) {
throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records");
}
return ts.get(0);
}
return null;
}

/**
*根據 Wrapper 條件,查詢總記錄數
*@param queryWrapper 實體物件封裝操作類(可以為 null)
*/
Long selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);


/**
*根據 entity 條件,查詢全部記錄
*@param queryWrapper 實體物件封裝操作類(可以為 null)
*/
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);


/**
*根據 Wrapper 條件,查詢全部記錄
*@param queryWrapper 實體物件封裝操作類(可以為 null)
*/
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
*根據 Wrapper 條件,查詢全部記錄
*<p>注意: 只返回第一個欄位的值</p>
*@param queryWrapper 實體物件封裝操作類(可以為 null)
*/
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);


/**
*根據 entity 條件,查詢全部記錄(並翻頁)
*@param page	分頁查詢條件(可以為 RowBounds.DEFAULT)
*@param queryWrapper 實體物件封裝操作類(可以為 null)
*/
<P extends IPage<T>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
*根據 Wrapper 條件,查詢全部記錄(並翻頁)
*@param page	分頁查詢條件
*@param queryWrapper 實體物件封裝操作類
*/
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}

2、插入

@Test
public void testInsert(){
User user = new User(null, "張三", 23, "[email protected]");
//INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? ) int result = userMapper.insert(user);
System.out.println("受影響行數:"+result);
//1475754982694199298
System.out.println("id自動獲取:"+user.getId());
}

最終執行的結果,所獲取的id為1475754982694199298
這是因為MyBatis-Plus在實現插入資料時,會預設基於雪花演算法的策略生成id

3、刪除

a>通過id刪除記錄

@Test
public void testDeleteById(){
//通過id刪除使用者資訊
//DELETE FROM user WHERE id=?
int result = userMapper.deleteById(1475754982694199298L); System.out.println("受影響行數:"+result);
}

b>通過id批量刪除記錄

@Test
public void testDeleteBatchIds(){
//通過多個id批量刪除
//DELETE FROM user WHERE id IN ( ? , ? , ? )
List<Long> idList = Arrays.asList(1L, 2L, 3L); int result = userMapper.deleteBatchIds(idList); System.out.println("受影響行數:"+result);
}

c>通過map條件刪除記錄

@Test
public void testDeleteByMap(){
//根據map集合中所設定的條件刪除記錄
//DELETE FROM user WHERE name = ? AND age = ? Map<String, Object> map = new HashMap<>(); map.put("age", 23);
map.put("name", "張三");
int result = userMapper.deleteByMap(map); System.out.println("受影響行數:"+result);
}

4、修改

@Test
public void testUpdateById(){
User user = new User(4L, "admin", 22, null);
//UPDATE user SET name=?, age=? WHERE id=? int result = userMapper.updateById(user); System.out.println("受影響行數:"+result);
}

5、查詢

a>根據id查詢使用者資訊

@Test
public void testSelectById(){
//根據id查詢使用者資訊
//SELECT id,name,age,email FROM user WHERE id=? User user = userMapper.selectById(4L); System.out.println(user);
}

b>根據多個id查詢多個使用者資訊

@Test
public void testSelectBatchIds(){
//根據多個id查詢多個使用者資訊
//SELECT id,name,age,email FROM user WHERE id IN ( ? , ? ) List<Long> idList = Arrays.asList(4L, 5L);
List<User> list = userMapper.selectBatchIds(idList); list.forEach(System.out::println);
}

c>通過map條件查詢使用者資訊

@Test
public void testSelectByMap(){
//通過map條件查詢使用者資訊
//SELECT id,name,age,email FROM user WHERE name = ? AND age = ? Map<String, Object> map = new HashMap<>();
map.put("age", 22);
map.put("name", "admin");
List<User> list = userMapper.selectByMap(map); list.forEach(System.out::println);
}

d>查詢所有資料

@Test
public void testSelectList(){
//查詢所有使用者資訊
//SELECT id,name,age,email FROM user List<User> list = userMapper.selectList(null); list.forEach(System.out::println);
}

通過觀察BaseMapper中的方法,大多方法中都有Wrapper型別的形參,此為條件構造器,可針 對於SQL語句設定不同的條件,若沒有條件,則可以為該形參賦值null,即查詢(刪除/修改)所 有資料

6、通用Service

說明:
通用 Service CRUD 封裝IService介面,進一步封裝 CRUD 採用

泛型 為任意實體物件

字首命名方式區分

層避免混淆,

建議如果存在自定義通用 Service 方法的可能,請建立自己的 繼承提供的基類
官網地址:https://baomidou.com/pages/49cc81/#service-crud-介面

a>IService

MyBatis-Plus中有一個介面 IService和其實現類 ServiceImpl,封裝了常見的業務層邏輯詳情檢視原始碼IService和ServiceImpl

b>建立Service介面和實現類

/**
* UserService繼承IService模板提供的基礎功能
*/
public interface UserService extends IService<User> {

}
/**
*ServiceImpl實現了IService,提供了IService中基礎功能的實現
*若ServiceImpl無法滿足業務需求,則可以使用自定的UserService定義方法,並在實現類中實現
*/ 
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

c>測試查詢記錄數

@Autowired
private UserService userService;


@Test
public void testGetCount(){
	long count = userService.count(); 
    System.out.println("總記錄數:" + count);
}

d>測試批量插入

@Test
public void testSaveBatch(){
// SQL長度有限制,海量資料插入單條SQL無法實行,
// 因此MP將批量插入放在了通用Service中實現,而不是通用Mapper ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 5; i++) { 
    User user = new User(); 
    user.setName("ybc" + i); 
    user.setAge(20 + i); users.add(user);
}
//SQL:INSERT INTO t_user ( username, age ) VALUES ( ?, ? ) userService.saveBatch(users);
}

三、常用註解

1、@TableName

經過以上的測試,在使用MyBatis-Plus實現基本的CRUD時,我們並沒有指定要操作的表,只是在
Mapper介面繼承BaseMapper時,設定了泛型User,而操作的表為user表
由此得出結論,MyBatis-Plus在確定操作的表時,由BaseMapper的泛型決定,即實體型別決定,且預設操作的表名和實體型別的類名一致

a>問題

若實體類型別的類名和要操作的表的表名不一致,會出現什麼問題?

我們將表user更名為t_user,測試查詢功能
程式丟擲異常,Table 'mybatis_plus.user' doesn't exist,因為現在的表名為t_user,而預設操作的表名和實體型別的類名一致,即user表

b>通過@TableName解決問題

在實體類型別上新增@TableName("t_user"),標識實體類對應的表,即可成功執行SQL語句

@TableName("t_user")

c>通過全域性配置解決問題

在開發的過程中,我們經常遇到以上的問題,即實體類所對應的表都有固定的字首,例如t_或tbl_
此時,可以使用MyBatis-Plus提供的全域性配置,為實體類所對應的表名設定預設的字首,那麼就 不需要在每個實體類上通過@TableName標識實體類對應的表

mybatis-plus: configuration:
# 配置MyBatis日誌
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config:
db-config:
# 配置MyBatis-Plus操作表的預設字首
table-prefix: t_

2、@TableId

經過以上的測試,MyBatis-Plus在實現CRUD時,會預設將id作為主鍵列,並在插入資料時,預設 基於雪花演算法的策略生成id

a>問題

若實體類和表中表示主鍵的不是id,而是其他欄位,例如uid,MyBatis-Plus會自動識別uid為主 鍵列嗎?
我們實體類中的屬性id改為uid,將表中的欄位id也改為uid,測試新增功能程式丟擲異常,Field 'uid' doesn't have a default value,說明MyBatis-Plus沒有將uid作為主鍵賦值

b>通過@TableId解決問題

在實體類中uid屬性上通過@TableId將其標識為主鍵,即可成功執行SQL語句

c>@TableId的value屬性

若實體類中主鍵對應的屬性為id,而表中表示主鍵的欄位為uid,此時若只在屬性id上添加註解@TableId,則丟擲異常Unknown column 'id' in 'field list',即MyBatis-Plus仍然會將id作為表的主鍵操作,而表中表示主鍵的是欄位uid
此時需要通過@TableId註解的value屬性,指定表中的主鍵欄位,@TableId("uid")或
@TableId(value="uid")

d>@TableId的type屬性

type屬性用來定義主鍵策略

常用的主鍵策略:

描述
IdType.ASSIGN_ID(預設) 基於雪花演算法的策略生成資料id,與資料庫id是否設定自增無關
IdType.AUTO 使用資料庫的自增策略,注意,該型別請確保資料庫設定了id自增, 否則無效

配置全域性主鍵策略:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto

e>雪花演算法

  • 背景

需要選擇合適的方案去應對資料規模的增長,以應對逐漸增長的訪問壓力和資料量。資料庫的擴充套件方式主要包括:業務分庫、主從複製,資料庫分表。

  • 資料庫分表

將不同業務資料分散儲存到不同的資料庫伺服器,能夠支撐百萬甚至千萬使用者規模的業務,但如果業務 繼續發展,同一業務的單表資料也會達到單臺數據庫伺服器的處理瓶頸。例如,淘寶的幾億使用者資料, 如果全部存放在一臺資料庫伺服器的一張表中,肯定是無法滿足效能要求的,此時就需要對單表資料進 行拆分。

單表資料拆分有兩種方式:垂直分表和水平分表。示意圖如下:

垂直分表

垂直分表適合將表中某些不常用且佔了大量空間的列拆分出去。
例如,前面示意圖中的 nickname 和 description 欄位,假設我們是一個婚戀網站,使用者在篩選其他使用者的時候,主要是用 age 和 sex 兩個欄位進行查詢,而 nickname 和 description 兩個欄位主要用於展示,一般不會在業務查詢中用到。description 本身又比較長,因此我們可以將這兩個欄位獨立到另外一張表中,這樣在查詢 age 和 sex 時,就能帶來一定的效能提升。

水平分表

水平分表適合錶行數特別大的表,有的公司要求單錶行數超過 5000 萬就必須進行分表,這個數字可以
作為參考,但並不是絕對標準,關鍵還是要看錶的訪問效能。對於一些比較複雜的表,可能超過1000
萬就要分表了;而對於一些簡單的表,即使儲存資料超過 1 億行,也可以不分表。但不管怎樣,當看到表的資料量達到千萬級別時,作為架構師就要警覺起來,因為這很可能是架構的性 能瓶頸或者隱患。

水平分表相比垂直分表,會引入更多的複雜性,例如要求全域性唯一的資料id該如何處理

主鍵自增

①以最常見的使用者 ID 為例,可以按照 1000000 的範圍大小進行分段,1 ~ 999999 放到表 1中, 1000000 ~ 1999999 放到表2中,以此類推。
②複雜點:分段大小的選取。分段太小會導致切分後子表數量過多,增加維護複雜度;分段太大可能會 導致單表依然存在效能問題,一般建議分段大小在 100 萬至 2000 萬之間,具體需要根據業務選取合適的分段大小。
③優點:可以隨著資料的增加平滑地擴充新的表。例如,現在的使用者是 100 萬,如果增加到 1000 萬, 只需要增加新的表就可以了,原有的資料不需要動。
④缺點:分佈不均勻。假如按照 1000 萬來進行分表,有可能某個分段實際儲存的資料量只有 1 條,而
另外一個分段實際儲存的資料量有 1000 萬條。

取模

①同樣以使用者 ID 為例,假如我們一開始就規劃了 10 個數據庫表,可以簡單地用 user_id % 10 的值來表示資料所屬的資料庫表編號,ID 為 985 的使用者放到編號為 5 的子表中,ID 為 10086 的使用者放到編號
為 6 的子表中。
②複雜點:初始表數量的確定。表數量太多維護比較麻煩,表數量太少又可能導致單表效能存在問題。
③優點:表分佈比較均勻。
④缺點:擴充新的表很麻煩,所有資料都要重分佈。

雪花演算法

雪花演算法是由Twitter公佈的分散式主鍵生成演算法,它能夠保證不同表的主鍵的不重複性,以及相同表的 主鍵的有序性。
①核心思想:
長度共64bit(一個long型)。
首先是一個符號位,1bit標識,由於long基本型別在Java中是帶符號的,最高位是符號位,正數是0,負 數是1,所以id一般是正數,最高位是0。
41bit時間截(毫秒級),儲存的是時間截的差值(當前時間截 - 開始時間截),結果約等於69.73年。
10bit作為機器的ID(5個bit是資料中心,5個bit的機器ID,可以部署在1024個節點)。
12bit作為毫秒內的流水號(意味著每個節點在每毫秒可以產生 4096 個 ID)。

②優點:整體上按照時間自增排序,並且整個分散式系統內不會產生ID碰撞,並且效率較高。

3、@TableField

經過以上的測試,我們可以發現,MyBatis-Plus在執行SQL語句時,要保證實體類中的屬性名和 表中的欄位名一致
如果實體類中的屬性名和欄位名不一致的情況,會出現什麼問題呢?

a>情況1

若實體類中的屬性使用的是駝峰命名風格,而表中的欄位使用的是下劃線命名風格例如實體類屬性userName,表中欄位user_name
此時MyBatis-Plus會自動將下劃線命名風格轉化為駝峰命名風格
相當於在MyBatis中配置

b>情況2

若實體類中的屬性和表中的欄位不滿足情況1 例如實體類屬性name,表中欄位username
此時需要在實體類屬性上使用@TableField("username")設定屬性所對應的欄位名

4、@TableLogic

a>邏輯刪除

  • 物理刪除:真實刪除,將對應資料從資料庫中刪除,之後查詢不到此條被刪除的資料
  • 邏輯刪除:假刪除,將對應資料中代表是否被刪除欄位的狀態修改為“被刪除狀態”,之後在資料庫中仍舊能看到此條資料記錄
  • 使用場景:可以進行資料恢復

b>實現邏輯刪除

step1:資料庫中建立邏輯刪除狀態列,設定預設值為0

step2:實體類中新增邏輯刪除屬性

step3:測試
測試刪除功能,真正執行的是修改
UPDATE t_user SET is_deleted=1 WHERE id=? AND is_deleted=0
測試查詢功能,被邏輯刪除的資料預設不會被查詢
SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0

四、條件構造器和常用介面

1、wapper介紹

  • Wrapper : 條件構造抽象類,最頂端父類
  • AbstractWrapper : 用於查詢條件封裝,生成 sql 的 where 條件
  • QueryWrapper : 查詢條件封裝UpdateWrapper : Update 條件封裝AbstractLambdaWrapper : 使用Lambda 語法
  • LambdaQueryWrapper :用於Lambda語法使用的查詢Wrapper LambdaUpdateWrapper : Lambda 更新封裝Wrapper

2、QueryWrapper

a>例1:組裝查詢條件

@Test
public void test01(){
//查詢使用者名稱包含a,年齡在20到30之間,並且郵箱不為null的使用者資訊
//SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (username LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
QueryWrapper<User> queryWrapper = new QueryWrapper<>(); 
    queryWrapper.like("username", "a")
				.between("age", 20, 30)
				.isNotNull("email");
	List<User> list = userMapper.selectList(queryWrapper); 
    list.forEach(System.out::println);
}

b>例2:組裝排序條件

@Test
public void test02(){
//按年齡降序查詢使用者,如果年齡相同則按id升序排列
//SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 ORDER BY age DESC,id ASC
QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper
.orderByDesc("age")
.orderByAsc("id");
List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println);
}

c>例3:組裝刪除條件

@Test
public void test03(){
//刪除email為空的使用者
//DELETE FROM t_user WHERE (email IS NULL) QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.isNull("email");
//條件構造器也可以構建刪除語句的條件
	int result = userMapper.delete(queryWrapper);
    System.out.println("受影響的行數:" + result);
}

d>例4:條件的優先順序

@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//將(年齡大於20並且使用者名稱中包含有a)或郵箱為null的使用者資訊修改
//UPDATE t_user SET age=?, email=? WHERE (username LIKE ? AND age > ? OR email IS NULL)
	queryWrapper.like("username", "a")
				.gt("age", 20)
				.or()
				.isNull("email"); 
	User user = new User(); user.setAge(18);
	user.setEmail("[email protected]");
	int result = userMapper.update(user, queryWrapper);
	System.out.println("受影響的行數:" + result);
}
@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//將使用者名稱中包含有a並且(年齡大於20或郵箱為null)的使用者資訊修改
//UPDATE t_user SET age=?, email=? WHERE (username LIKE ? AND (age > ? OR email IS NULL))
//lambda表示式內的邏輯優先運算
	queryWrapper.like("username", "a")
			.and(i -> i.gt("age", 20)
			.or()
			.isNull("email"));
	User user = new User();
	user.setAge(18); 
	user.setEmail("[email protected]");
	int result = userMapper.update(user, queryWrapper);
	System.out.println("受影響的行數:" + result);
}

e>例5:組裝select子句

@Test
public void test05() {
//查詢使用者資訊的username和age欄位
//SELECT username,age FROM t_user
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("username", "age");
//selectMaps()返回Map集合列表,通常配合select()使用,避免User物件中沒有被查詢到的列值為null
	List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper); 
    maps.forEach(System.out::println);
}

f>例6:實現子查詢

@Test
public void test06() {
//查詢id小於等於3的使用者資訊
//SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE (id IN (select id from t_user where id <= 3))
	QueryWrapper<User> queryWrapper = new QueryWrapper<>(); 
    queryWrapper.inSql("id", "select id from t_user where id <= 3");
    List<User> list = userMapper.selectList(queryWrapper);
    list.forEach(System.out::println);
}

3、UpdateWrapper

updateWrapper.set("age", 18)
.set("email", "[email protected]")
.like("username", "a")
.and(i -> i.gt("age", 20).or().isNull("email"));

在這裡,.and()方法中是一個消費者介面,其泛型是一個QueryWrapper,泛型的型別就是該消費者介面accept方法中的引數,i代表的就是引數,也就是i就是QueryWrapper,箭頭後面就是對i的操作,即是函式體。

@Test
public void test07() {
//將(年齡大於20或郵箱為null)並且使用者名稱中包含有a的使用者資訊修改
//組裝set子句以及修改條件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();//lambda表示式內的邏輯優先運算
    updateWrapper.set("age", 18)
				.set("email", "[email protected]")
				.like("username", "a")
				.and(i -> i.gt("age", 20).or().isNull("email"));
//這裡必須要建立User物件,否則無法應用自動填充。如果沒有自動填充,可以設定為null
//UPDATE t_user SET username=?, age=?,email=? WHERE (username LIKE ? AND (age > ? OR email IS NULL))
//User user = new User();
//user.setName("張三");
//int result = userMapper.update(user, updateWrapper);
//UPDATE t_user SET age=?,email=? WHERE (username LIKE ? AND (age > ? OR email IS NULL))
	int result = userMapper.update(null, updateWrapper); 
    System.out.println(result);
}

4、condition

在真正開發的過程中,組裝條件是常見的功能,而這些條件資料來源於使用者輸入,是可選的,因 此我們在組裝這些條件時,必須先判斷使用者是否選擇了這些條件,若選擇則需要組裝該條件,若 沒有選擇則一定不能組裝,以免影響SQL執行的結果

思路一:

@Test
public void test08() {
//定義查詢條件,有可能為null(使用者未輸入或未選擇) 
    String username = null;
	Integer ageBegin = 10;
    Integer ageEnd = 24;
	QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//StringUtils.isNotBlank()判斷某字串是否不為空且長度不為0且不由空白符(whitespace)構成
if(StringUtils.isNotBlank(username)){ queryWrapper.like("username","a");
}
if(ageBegin != null){ queryWrapper.ge("age", ageBegin);
}
if(ageEnd != null){ queryWrapper.le("age", ageEnd);
}
//SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE (age >=? AND age <= ?)
	List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}

思路二:

上面的實現方案沒有問題,但是程式碼比較複雜,我們可以使用帶condition引數的過載方法構建查 詢條件,簡化程式碼的編寫

@Test
public void test08UseCondition() {
	//定義查詢條件,有可能為null(使用者未輸入或未選擇) 
    String username = null;
	Integer ageBegin = 10; 
    Integer ageEnd = 24;
	QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    //StringUtils.isNotBlank()判斷某字串是否不為空且長度不為0且不由空白符(whitespace)構成
	queryWrapper.like(StringUtils.isNotBlank(username), "username", "a")
				.ge(ageBegin != null, "age", ageBegin)
				.le(ageEnd != null, "age", ageEnd);
//SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE (age >=? AND age <= ?)
	List<User> users = userMapper.selectList(queryWrapper); 
    users.forEach(System.out::println);
}

5、LambdaQueryWrapper

User::getName:通過函式式介面,訪問實體類屬性,從而通過該屬性得到對應的欄位名

@Test
public void test09() {
//定義查詢條件,有可能為null(使用者未輸入) 
    String username = "a";
	Integer ageBegin = 10;
    Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//避免使用字串表示欄位,防止執行時錯誤
    queryWrapper.like(StringUtils.isNotBlank(username), User::getName, username)
				.ge(ageBegin != null, User::getAge, ageBegin)
				.le(ageEnd != null, User::getAge, ageEnd);
    List<User> users = userMapper.selectList(queryWrapper); 
    users.forEach(System.out::println);
}

五、外掛

1、分頁外掛

MyBatis Plus自帶分頁外掛,只要簡單的配置即可實現分頁功能

a>新增配置類

@Configuration
@MapperScan("com.ekertree.mybatisplus.mapper")
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

b>測試

    @Test
    public void testPage() {
        //設定分頁引數
        Page<User> page = new Page<>(1, 5);
        userMapper.selectPage(page, null);
        //獲取分頁資料
        List<User> list = page.getRecords();
        list.forEach(System.out::println);
        System.out.println("當前頁:" + page.getCurrent());
        System.out.println("每頁顯示的條數:" + page.getSize());
        System.out.println("總記錄數:" + page.getTotal());
        System.out.println("總頁數:" + page.getPages());
        System.out.println("是否有上一頁:" + page.hasPrevious());
        System.out.println("是否有下一頁:" + page.hasNext());
    }

測試結果:
User(id=1, name=Jone, age=18, [email protected], isDeleted=0) User(id=2, name=Jack, age=20, [email protected], isDeleted=0) User(id=3, name=Tom, age=28, [email protected], isDeleted=0) User(id=4, name=Sandy, age=21, [email protected], isDeleted=0) User(id=5, name=Billie, age=24, email=test5@ba omidou.com, isDeleted=0) 當前頁:1 每頁顯示的條數:5 總記錄數:17 總頁數:4 是否有上一頁:false 是否有下一頁:true

2、xml自定義分頁

a>UserMapper中定義介面方法

/**
*根據年齡查詢使用者列表,分頁顯示
*@param page 分頁物件,xml中可以從裡面進行取值,傳遞引數 Page 即自動分頁,必須放在第一位
*@param age 年齡
*@return
*/ 
IPage<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);

b>UserMapper.xml中編寫SQL

<!--SQL片段,記錄基礎欄位-->
<sql id="BaseColumns">id,username,age,email</sql>


<!--IPage<User> selectPageVo(Page<User> page, Integer age);-->
<select id="selectPageVo" resultType="User">
SELECT <include refid="BaseColumns"></include> FROM t_user WHERE age > #
{age}
</select>

c>測試

    @Test
    public void testSelectPageVo() {
        //設定分頁引數
        Page<User> page = new Page<>(1, 5);
        userMapper.selectPageVo(page, 20);
        //獲取分頁資料
        List<User> list = page.getRecords();
        list.forEach(System.out::println);
        System.out.println("當前頁:" + page.getCurrent());
        System.out.println("每頁顯示的條數:" + page.getSize());
        System.out.println("總記錄數:" + page.getTotal());
        System.out.println("總頁數:" + page.getPages());
        System.out.println("是否有上一頁:" + page.hasPrevious());
        System.out.println("是否有下一頁:" + page.hasNext());
    }

結果:
User(id=3, name=Tom, age=28, [email protected], isDeleted=null) User(id=4, name=Sandy, age=21, [email protected], isDeleted=null) User(id=5, name=Billie, age=24, [email protected], isDeleted=null) User(id=8, name=ybc1, age=21, email=null, isDeleted=null) User(id=9, name=ybc2, age=22, email=null, isDeleted=null) 當前頁:1 每頁顯示的條數:5 總記錄數:12 總頁數:3 是否有上一頁:false 是否有下一頁:true

3、樂觀鎖

a>場景

一件商品,成本價是80元,售價是100元。老闆先是通知小李,說你去把商品價格增加50元。小 李正在玩遊戲,耽擱了一個小時。正好一個小時後,老闆覺得商品價格增加到150元,價格太高,可能會影響銷量。又通知小王,你把商品價格降低30元。
此時,小李和小王同時操作商品後臺系統。小李操作的時候,系統先取出商品價格100元;小王 也在操作,取出的商品價格也是100元。小李將價格加了50元,並將100+50=150元存入了資料庫;小王將商品減了30元,並將100-30=70元存入了資料庫。是的,如果沒有鎖,小李的操作就 完全被小王的覆蓋了。
現在商品價格是70元,比成本價低10元。幾分鐘後,這個商品很快出售了1千多件商品,老闆虧1 萬多。

b>樂觀鎖與悲觀鎖

上面的故事,如果是樂觀鎖,小王儲存價格前,會檢查下價格是否被人修改過了。如果被修改過 了,則重新取出的被修改後的價格,150元,這樣他會將120元存入資料庫。
如果是悲觀鎖,小李取出資料後,小王只能等小李操作完之後,才能對價格進行操作,也會保證 最終的價格是120元。

c>樂觀鎖實現流程

資料庫中新增version欄位
取出記錄時,獲取當前version

SELECT id,`name`,price,`version` FROM product WHERE id=1

更新時,version + 1,如果where語句中的version版本不對,則更新失敗

UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND
`version`=1

d>Mybatis-Plus實現樂觀鎖

修改實體類
package com.atguigu.mybatisplus.entity;


import com.baomidou.mybatisplus.annotation.Version; import lombok.Data;

@Data
public class Product { 
	private Long id; 
	
	private String name; 
	
	private Integer price;
	
	@Version
	private Integer version;
}
新增樂觀鎖外掛配置
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){ 
	MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
	//新增分頁外掛
	interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
	//新增樂觀鎖外掛
	interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); 
    return interceptor;
}
測試修改衝突

小李查詢商品資訊:
SELECT id,name,price,version FROM t_product WHERE id=?
小王查詢商品資訊:
SELECT id,name,price,version FROM t_product WHERE id=?
小李修改商品價格,自動將version+1
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=?
Parameters: 外星人筆記本(String), 150(Integer), 1(Integer), 1(Long), 0(Integer)
小王修改商品價格,此時version已更新,條件不成立,修改失敗
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=?
Parameters: 外星人筆記本(String), 70(Integer), 1(Integer), 1(Long), 0(Integer)
最終,小王修改失敗,查詢價格:150
SELECT id,name,price,version FROM t_product WHERE id=?

優化流程
@Test
public void testConcurrentVersionUpdate() {

//小李取資料
Product p1 = productMapper.selectById(1L);

//小王取資料
Product p2 = productMapper.selectById(1L);

//小李修改 + 50
p1.setPrice(p1.getPrice() + 50);
int result1 = productMapper.updateById(p1); 
System.out.println("小李修改的結果:" + result1);

//小王修改 - 30
p2.setPrice(p2.getPrice() - 30);
int result2 = productMapper.updateById(p2);
System.out.println("小王修改的結果:" + result2); 
if(result2 == 0){
//失敗重試,重新獲取version並更新
p2 = productMapper.selectById(1L);
p2.setPrice(p2.getPrice() - 30); 
result2 = productMapper.updateById(p2);
}
System.out.println("小王修改重試的結果:" + result2);

//老闆看價格
Product p3 = productMapper.selectById(1L);
System.out.println("老闆看價格:" + p3.getPrice());
}

六、通用列舉

表中的有些欄位值是固定的,例如性別(男或女),此時我們可以使用MyBatis-Plus的通用列舉 來實現

a>資料庫表新增欄位sex

b>建立通用列舉型別

@Getter
public enum SexEnum { 
MALE(1, "男"),
FEMALE(2, "女");


@EnumValue
private Integer sex; 
private String sexName;

    SexEnum(Integer sex, String sexName) { 
        this.sex = sex;
        this.sexName = sexName;
    }
}

c>配置掃描通用列舉

mybatis-plus: configuration:
# 配置MyBatis日誌
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config:
db-config:
# 配置MyBatis-Plus操作表的預設字首
table-prefix: t_
# 配置MyBatis-Plus的主鍵策略
id-type: auto # 配置掃描通用列舉
type-enums-package: com.atguigu.mybatisplus.enums

d>測試

@Test
public void testSexEnum(){ 
	User user = new User(); 
	user.setName("Enum"); 
	user.setAge(20);
	//設定性別資訊為列舉項,會將@EnumValue註解所標識的屬性值儲存到資料庫
	user.setSex(SexEnum.MALE);
	//INSERT INTO t_user ( username, age, sex ) VALUES ( ?, ?, ? )
	//Parameters: Enum(String), 20(Integer), 1(Integer) userMapper.insert(user);
}

七、程式碼生成器

1、引入依賴

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

2、快速生成

@SpringBootTest
public class FastAutoGeneratorTest {

    @Test
    void test(){
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&userSSL=false", "root", "root")
                .globalConfig(builder -> {
                    builder.author("ekertree") // 設定作者
                            .enableSwagger() // 開啟 swagger 模式
                            .fileOverride() // 覆蓋已生成檔案
                            .outputDir("D://test"); // 指定輸出目錄
                })
                .packageConfig(builder -> {
                    builder.parent("com.ekertree") // 設定父包名
                            .moduleName("mybatisplus") // 設定父包模組名
                            .pathInfo(Collections.singletonMap(OutputFile.mapper, "D://test")); // 設定mapperXml生成路徑
                })
                .strategyConfig(builder -> {
                    builder.addInclude("user") // 設定需要生成的表名
                            .addTablePrefix("t_", "c_"); // 設定過濾表字首
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,預設的是Velocity引擎模板
                .execute();
    }
}

八、多資料來源

適用於多種場景:純粹多庫、 讀寫分離、 一主多從、 混合模式等目前我們就來模擬一個純粹多庫的一個場景,其他場景類似
場景說明:
我們建立兩個庫,分別為:mybatis_plus(以前的庫不動)與mybatis_plus_1(新建),將mybatis_plus庫的product表移動到mybatis_plus_1庫,這樣每個庫一張表,通過一個測試用例 分別獲取使用者資料與商品資料,如果獲取到說明多庫模擬成功

1、建立資料庫及表

建立資料庫mybatis_plus_1和表product

CREATE DATABASE `mybatis_plus_1` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; use `mybatis_plus_1`;
CREATE TABLE product (
id BIGINT(20) NOT NULL COMMENT '主鍵ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名稱', price INT(11) DEFAULT 0 COMMENT '價格',
version INT(11) DEFAULT 0 COMMENT '樂觀鎖版本號', PRIMARY KEY (id)
);

新增測試資料

INSERT INTO product (id, NAME, price) VALUES (1, '外星人筆記本', 100);

2、引入依賴

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>

3、配置多資料來源

說明:註釋掉之前的資料庫連線,新增新配置

spring:
  # 配置資料來源資訊
  datasource:
    dynamic:
      primary: master #設定預設的資料來源或者資料來源組,預設值即為master
      strict: false #嚴格匹配資料來源,預設false. true未匹配到指定資料來源時拋異常,false使用預設資料來源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncodeing=utf-8&useSSL=false
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0開始支援SPI可省略此配置
        slave_1:
          url: jdbc:mysql://localhost:3306/mybatis_plus_1?characterEncodeing=utf-8&useSSL=false
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver

4、建立使用者service

public interface UserService extends IService<User> {
}
@Service
@DS("master")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

5、建立商品service

public interface ProductService extends IService<Product> {
}
@Service
@DS("slave_1")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}

6、測試

@SpringBootTest
class MybatisPlusDatasourceApplicationTests {

    @Autowired
    private UserService userService;

    @Autowired
    private ProductService productService;

    @Test
    void contextLoads() {
        User user = userService.getById(1);
        System.out.println(user);
        Product product = productService.getById(1);
        System.out.println(product);
    }

}

結果:
1、都能順利獲取物件,則測試成功
2、如果我們實現讀寫分離,將寫操作方法加上主庫資料來源,讀操作方法加上從庫資料來源,自動切 換,是不是就能實現讀寫分離?

九、MyBatisX外掛

MyBatis-Plus為我們提供了強大的mapper和service模板,能夠大大的提高開發效率
但是在真正開發過程中,MyBatis-Plus並不能為我們解決所有問題,例如一些複雜的SQL,多表 聯查,我們就需要自己去編寫程式碼和SQL語句,我們該如何快速的解決這個問題呢,這個時候可 以使用MyBatisX外掛
MyBatisX一款基於 IDEA 的快速開發外掛,為效率而生。

MyBatisX外掛用法:https://baomidou.com/pages/ba5b24/