jenkins的 Active choises parameter外掛進行動態引數選擇
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欄位
取出記錄時,獲取當前versionSELECT 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/