這一篇從一個入門的基本體驗介紹,再到對於 CRUD 的一個詳細介紹,在介紹過程中將涉及到的一些問題,例如逐漸策略,自動填充,樂觀鎖等內容說了一下,只選了一些重要的內容,還有一些沒提及到,具體可以參考官網,簡單的看完,其實會發現,如果遇到單表的 CRUD ,直接用 MP 肯定舒服,如果寫多表,還是用 Mybatis 多點,畢竟直接寫 SQL 會直觀一點,MP 給我的感覺,就是方法封裝了很多,還有一些算比較是用的外掛,但是可讀性會稍微差一點,不過個人有個人的看法哇,祝大家國慶快樂 ~
一 引言
最初的 JDBC,我們需要寫大量的程式碼來完成與基本的 CRUD ,或許會在一定程度上使用 Spring 的 JdbcTemplate 或者 Apache 的 DBUtils ,這樣一些對 JDBC 的簡單封裝的工具類。
再到後再使用 Mybatis 等一些優秀的持久層框架,大大的簡化了開發,我們只需要使用一定的 XML 或者註解就可以完成原來的工作
JDBC --> Mybatis 無疑簡化了開發者的工作,而今天我們所講額 MyBatis-Plus 就是在 MyBatis 的基礎上,更加的簡化開發,來一起看看吧!
二 初識 MyBatis-Plus
下列介紹來自官網:
(一) 概述
MyBatis-Plus(簡稱 MP)是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
我們的願景是成為 MyBatis 最好的搭檔,就像魂鬥羅中的 1P、2P,基友搭配,效率翻倍。
總之一句話:MyBatis-Plus —— 為簡化開發而生!
(二) 特性
- 無侵入:只做增強不做改變,引入它 不會 對現有工程產生影響,如絲般順滑
- 損耗小:啟動即會自動注入基本 CRUD,效能基本無損耗,直接物件導向操作
- 強大的 CRUD 操作:內建通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求
- 支援 Lambda 形式呼叫:通過 Lambda 表示式,方便的編寫各類查詢條件,無需再擔心欄位寫錯
- 支援主鍵自動生成:支援多達 4 種主鍵策略(內含分散式唯一 ID 生成器 - Sequence),可自由配置,完美解決主鍵問題
- 支援 ActiveRecord 模式:支援 ActiveRecord 形式呼叫,實體類只需繼承 Model 類即可進行強大的 CRUD 操作
- 支援自定義全域性通用操作:支援全域性通用方法注入( Write once, use anywhere )
- 內建程式碼生成器:採用程式碼或者 Maven 外掛可快速生成 Mapper 、 Model 、 Service 、 Controller 層程式碼,支援模板引擎,更有超多自定義配置等您來使用
- 內建分頁外掛:基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好外掛之後,寫分頁等同於普通 List 查詢
- 分頁外掛支援多種資料庫:支援 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多種資料庫
- 內建效能分析外掛:可輸出 Sql 語句以及其執行時間,建議開發測試時啟用該功能,能快速揪出慢查詢
- 內建全域性攔截外掛:提供全表 delete 、 update 操作智慧分析阻斷,也可自定義攔截規則,預防誤操作
(三) 支援資料庫
- mysql 、mariadb 、oracle 、db2 、h2 、hsql 、sqlite 、postgresql 、sqlserver 、presto 、Gauss 、Firebird
- Phoenix 、clickhouse 、Sybase ASE 、 OceanBase 、達夢資料庫 、虛谷資料庫 、人大金倉資料庫 、南大通用資料庫
三 入門初體驗
按照官網的案例簡單試一下 ,注:官網是基於 Springboot 的示例
@Repository
public interface UserMapper extends BaseMapper<User> {
}
(一) 建立入門案例表
@Repository
public interface UserMapper extends BaseMapper<User> {
}
自行建立一個資料庫即可,然後匯入官網給出的案例表,然後插入如下資料
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年齡',
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '郵箱',
PRIMARY KEY (`id`) USING BTREE
);
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'Jone', 18, 'test1@baomidou.com');
INSERT INTO `user` VALUES (2, 'Jack', 20, 'test2@baomidou.com');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'test3@baomidou.com');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'test4@baomidou.com');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'test5@baomidou.com');
(二) 初始化 SpringBoot 專案
如果沒有接觸過 SpringBoot,使用常規的 SSM 也是可以的,為了演示方便,這裡還是使用了SpringBoot,如果想在 SSM 中使用,一個注意依賴的修改,還一個就需要修改 xml 中的一些配置
(1) 引入依賴
引入 MyBatis-Plus-boot-starter 肯定是沒什麼疑問的,同樣我們還需要引入,資料庫連線的驅動依賴,還可以看需要引入 lombok,這裡為了簡便所以使用了它,如果不想使用,手動生成構造方法和 get set 即可
<!-- 資料庫驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
(2) 配置資料庫資訊
如何進行資料庫相關的資訊在以前的SpringBoot文章已經說過了,這裡強調一下:
mysql 5 驅動:com.mysql.jdbc.Driver
mysql 8 驅動:com.mysql.cj.jdbc.Driver
、還需要增加時區的配置
serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root99
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
(3) 建立實體類
根據資料庫欄位建立出對應實體屬性就行了,還是提一下:上方三個註解,主要是使用了 lombok 自動的生成那些 get set 等方法,不想用的同學直接自己按原來的方法顯式的寫出來就可以了~
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
}
(4) 建立Mapper介面
程式碼如下,可以看到,我們額外的繼承了 BaseMapper,同時指定了泛型為 User
@Repository
public interface UserMapper extends BaseMapper<User> {
}
其實點進去 BaseMapper 看一下,你會發現,在其中已經定義了關於 CRUD 一些基本方法還有一些涉及到配合條件實現更復雜的操作,同時泛型中指定的實體,會在增刪改查的方法中被呼叫
照這樣說的話,好像啥東西都被寫好了,如果現在想要進行一個簡單的增刪改查,是不是直接使用就行了
(5) 測試
首先在測試類中注入 UserMapper,這裡演示一個查詢所有的方法,所以使用了 selectList
,其引數是一個條件,這裡先置為空。
如果有哪些方法的使用不明確,我們可以先點到 BaseMapper 中去看一下,down 下原始碼以後,會有一些註釋說明
/**
* 根據 entity 條件,查詢全部記錄
*
* @param queryWrapper 實體物件封裝操作類(可以為 null)
*/
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
下面是測試查詢所有的全程式碼
@SpringBootTest
class MybatisPlusApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
List<User> userList = userMapper.selectList(null);
for (User user : userList) {
System.out.println(user);
}
}
}
(5) 結果
控制檯輸出如下
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)
經過一個簡單的測試,感覺還是很香的,而以前在 Mybatis 中我們執行sql語句時,是可以看到控制檯列印的日誌的,而這裡顯然沒有,其實通過一行簡單的配置就可以了
(三) 開啟日誌
其實只需要在配置檔案中加入短短的一行就可以了
MyBatis-Plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
列印如下:
當然你還可以通過一些 MyBatis Log 的外掛,來快速的檢視自己所執行的 sql
四 CRUD
(一) 插入操作
(1) 可用方法
首先,先試試插入一個實體的操作,我們選擇使用了 insert 這個方法,下面是其定義:
/**
* 插入一條記錄
*
* @param entity 實體物件
*/
int insert(T entity);
(2) 演示
@Test
public void testInsert() {
// 擬一個物件
User user = new User();
user.setName("理想二旬不止");
user.setAge(30);
user.setEmail("ideal_bwh@163.com");
// 插入操作
int count = userMapper.insert(user);
System.out.println(count);
System.out.println(user);
}
結果:
根據結果看到,插入確實成功了,但是一個發矇的問題出現了,為啥 id 變成了一個 long 型別的值
(3) 主鍵生成策略
對於主鍵的生成,官網有如下的一句話:
自3.3.0開始,預設使用雪花演算法+UUID(不含中劃線)
也就是說,因為上面我們沒有做任何的處理,所以它使用了預設的演算法來當做主鍵 id
A:雪花演算法(snowflake)
snowflake是Twitter開源的分散式ID生成演算法,結果是一個long型的ID。其核心思想是:使用41bit作為毫秒數,10bit作為機器的ID(5個bit是資料中心,5個bit的機器ID),12bit作為毫秒內的流水號(意味著每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。
雪花演算法 + UUID 所以基本是可以保證唯一的
當然除了雪花演算法為,我們還有一些別的主鍵生成的策略,例如 Redis、資料庫自增
對於我們之前常用的一種主鍵生成方式,一般都會用到資料庫id自增
(4) 設定主鍵自增
- 資料庫主鍵欄位設定自增!!!
- 主鍵實體類欄位註解
@TableId(type = IdType.AUTO)
再次插入,發現 id 已經實現了自增
(5) 欄位註解解釋
@TableId 註解中的屬性 Type 的值來自於 IdType 這個列舉類,其中我把每一項簡單解釋一下
-
AUTO(0)
:資料庫 ID 自增(MySQL 正常,Oracle 未測試)- 如果你想要全域性都使用 AUTO 這樣的資料庫自增方式,可以直接在 application.properties 中新增
MyBatis-Plus.global-config.db-config.id-type=auto
-
NONE(1)
:該型別為未設定主鍵型別(註解裡等於跟隨全域性,全域性裡約等於 INPUT) -
INPUT(2)
:使用者輸入 ID,也可以自定義輸入策略,內建策略如下- DB2KeyGenerator
- H2KeyGenerator
- KingbaseKeyGenerator
- OracleKeyGenerator
- PostgreKeyGenerator
使用時:
先新增 @Bean,然後實體類配置主鍵 Sequence,指定主鍵策略為 IdType.INPUT 即可,重點不說這個,有需要可以直接扒官網
@Bean
public IKeyGenerator keyGenerator() {
return new H2KeyGenerator();
}
-
ASSIGN_ID(3)
:雪花演算法 -
ASSIGN_UUID(4)
:不含中劃線的UUID
3.3.0 後,ID_WORKER(3)、ID_WORKER_STR(3)、UUID(4) 就已經被棄用了,前兩個可以使用 ASSIGN_ID(3)代替,最後一個使用 ASSIGN_UUID(4)代替
(二) 更新操作
(1) 可用方法
// 根據 whereEntity 條件,更新記錄
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
// 根據 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);
(2) 演示
MyBatis-Plus 中的更新操作也是非常方便
舉一種比較常見的一種情況,通過 id 值修改某些欄位
傳統做法會傳一個修改後的物件,然後通過 #{}
設定具體更新的值和 id
<update id="updateById">
UPDATE user SET name=#{name}, portrait=#{portrait}, gender=#{gender}, telephone=#{telephone}, email=#{email} WHERE id=#{id}
</update>
MyBatis-Plus 方式:
@Test
public void testUpdate() {
// 擬一個物件
User user = new User();
user.setId(1L);
user.setName("理想二旬不止");
user.setAge(20);
int i = userMapper.updateById(user);
System.out.println(i);
}
首先我們給定了 id 值,同時又修改了姓名和年齡這兩個欄位,但是並不是全部欄位,來看一下執行效果
神奇的發現,我們不需要在 sql 中進行設定了,所有的配置都被自動做好了,更新的內容和 id 都被自動填充好了
(3) 自動填充
自動填充是填充什麼內容呢?首先我們需要知道,一般來說表中的建立時間,修改時間,我們總是希望能夠給根據插入或者修改的時間自動填充,而不需要我們手動的去更新
可能以前的專案不是特別綜合或需要等原因,有時候也不會去設定建立時間等欄位,寫這部分是因為,在阿里巴巴的Java開發手冊(第5章 MySQL 資料庫 - 5.1 建表規約 - 第 9 條 )有明確指出:
【強制】表必備三欄位:id, create_time, update_time。
說明:其中 id 必為主鍵,型別為 bigint unsigned、單表時自增、步長為 1。create_time, update_time
的型別均為 datetime 型別,前者現在時表示主動式建立,後者過去分詞表示被動式更新
A:資料庫級別(不常用)
我們可以通過直接修改資料庫中對應欄位的預設值,來實現資料庫級別的自動新增語句
例如上圖中我首先新增了 create_time, update_time 兩個欄位,然後將型別選擇為 datetime
,又設定其預設值為 CURRENT_TIMESTAMP
注:更新時間欄位中要勾選 On Update Current_Timestamp ,插入不用,使用 SQLYog 沒問題,在 Navicat 某個版本下直接通過視覺化操作可能會報錯,沒有此預設值,這種情況就把表先匯出來,然後修改SQL,在SQL 中修改語句
create_time` datetime(0) DEFAULT CURRENT_TIMESTAMP(0) COMMENT '建立時間',
update_time` datetime(0) DEFAULT CURRENT_TIMESTAMP(0) COMMENT '修改時間',
B:程式碼級別
根據官網的自動填充功能的說明,其實我們需要做的只有兩點:
- 為 create_time, update_time 兩個欄位配置註解
- 自定義實現類 MyMetaObjectHandler
注:開始前,別忘了刪除剛才資料庫級別測試時的欄位預設值等喔
首先填充欄位註解:
@TableField(fill = FieldFill.INSERT)
@TableField(fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
FieldFill 說明:
public enum FieldFill {
/**
* 預設不處理
*/
DEFAULT,
/**
* 插入時填充欄位
*/
INSERT,
/**
* 更新時填充欄位
*/
UPDATE,
/**
* 插入和更新時填充欄位
*/
INSERT_UPDATE
}
接著建立自定義實現類 MyMetaObjectHandler,讓其實現MetaObjectHandler,重寫其 insertFill 和 updateFill 方法,列印日誌就不說了,通過 setFieldValByName 就可以對欄位進行賦值,原始碼中這個方法有三個引數
/**
* 通用填充
*
* @param fieldName java bean property name
* @param fieldVal java bean property value
* @param metaObject meta object parameter
*/
default MetaObjectHandler setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) {...}
- fieldName:欄位名
- fieldVal:該欄位賦予的值
- metaObject:操作哪個資料
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
setFieldValByName("createTime", new Date(), metaObject);
setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
setFieldValByName("updateTime", new Date(), metaObject);
}
}
檢視一下效果:
下面還有一些注意事項:
注意事項:
- 填充原理是直接給
entity
的屬性設定值!!!- 註解則是指定該屬性在對應情況下必有值,如果無值則入庫會是
null
MetaObjectHandler
提供的預設方法的策略均為:如果屬性有值則不覆蓋,如果填充值為null
則不填充- 欄位必須宣告
TableField
註解,屬性fill
選擇對應策略,該宣告告知MyBatis-Plus
需要預留注入SQL
欄位- 填充處理器
MyMetaObjectHandler
在 Spring Boot 中需要宣告@Component
或@Bean
注入- 要想根據註解
FieldFill.xxx
和欄位名
以及欄位型別
來區分必須使用父類的strictInsertFill
或者strictUpdateFill
方法- 不需要根據任何來區分可以使用父類的
fillStrategy
方法
(4) 樂觀鎖外掛
演示樂觀鎖外掛前,首先補充一些基礎概念:
A:沒有鎖會怎麼樣
打個比方,一張電影票價格為 30,老闆告訴員工 A ,把價格上調到 50,員工 A 因為有事耽擱了兩個小時,但是老闆想了一會覺得提價太高了,就想著定價 40 好了,正好碰到員工 B,就讓員工 B 將價格降低 10 塊
當正好兩個員工都在操作後臺系統時,兩人同時取出當前價格,即 30 元,員工A 先操作後 價格變成了 50元,但是員工 B 又將30 - 10 ,即 變成20塊,執行了更新操作,此時員工 B 的更新操作就會把前面的 50 元覆蓋掉,即最終成為了 20元,雖然我內心毫無波瀾,但老闆卻虧的一匹
B:樂觀鎖和悲觀鎖
- 樂觀鎖:故名思意十分樂觀,它總是認為不會出現問題,無論幹什麼不去上鎖!如果出現了問題,
再次更新值測試- 樂觀鎖下,員工 B 更新時會檢查是否這個價格已經被別人修改過了,如果是就會取出新的值,再操作
- 悲觀鎖:故名思意十分悲觀,它總是認為總是出現問題,無論幹什麼都會上鎖!再去操作!
- 悲觀鎖下,員工 B 只能在 員工 A 操作完以後才能操作,這樣能保證資料只有一個人在操作
C:MP 中的樂觀鎖外掛
意圖:
當要更新一條記錄的時候,希望這條記錄沒有被別人更新
樂觀鎖實現方式:
- 取出記錄時,獲取當前version
- 更新時,帶上這個version
- 執行更新時, set version = newVersion where version = oldVersion
- 如果version不對,就更新失敗
實現這個功能,只需要兩步:
- 新增資料庫version欄位和實體欄位以及註解
@Version // 樂觀鎖的Version註解
private Integer version;
- 建立配置類,引入樂觀鎖外掛
// 掃描 mapper 資料夾
@MapperScan("cn.ideal.mapper")
@EnableTransactionManagement
// 代表配置類
@Configuration
public class MyBatisPlusConfig {
// 樂觀鎖外掛
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
說明:剛開始例如掃描 mapper 這樣的註解就放在了啟動類中,現在有了配置類,所以把它也移過來了
測試一下:
首先1號和2號獲取到的資料是一樣的,但是在1號還沒有執行到更新的時候,2號搶先提交了更新操作,也就是說,當前真實資料已經是被2號修改過的了,與1號前面獲取到的不一致了
如果沒有樂觀鎖,那麼2號提交的更新會被1號的更新資料覆蓋
// 測試更新
@Test
public void testUpdate() {
// 1號取得了資料
User user1 = userMapper.selectById(1L);
user1.setName("樂觀鎖1號");
user1.setAge(20);
user1.setEmail("ideal_bwh@xxx.com");
// 2號取得了資料
User user2 = userMapper.selectById(1L);
user2.setName("樂觀鎖2號");
user2.setAge(30);
user2.setEmail("ideal@xxx.com");
// 2號提交更新
userMapper.updateById(user2);
// 1號提交更新
userMapper.updateById(user1);
}
可以看到,在2號搶先執行後,1號就沒有成功執行了
同樣資料庫中表的其 version 也從1變成了1
(三) 查詢操作
(1) 可用方法
// 根據 ID 查詢
T selectById(Serializable id);
// 根據 entity 條件,查詢一條記錄
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查詢(根據ID 批量查詢)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根據 entity 條件,查詢全部記錄
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查詢(根據 columnMap 條件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根據 Wrapper 條件,查詢全部記錄
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根據 Wrapper 條件,查詢全部記錄。注意: 只返回第一個欄位的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根據 entity 條件,查詢全部記錄(並翻頁)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根據 Wrapper 條件,查詢全部記錄(並翻頁)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根據 Wrapper 條件,查詢總記錄數
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
(2) 簡單查詢
A:根據 id 查詢
@Test
public void testSelectById(){
User user = userMapper.selectById(1L);
System.out.println(user);
}
B:根據 id 集合查詢
說明:我這裡使用的還是最基本的寫法,例如 List 可以用工具類建立 如:Arrays.asList(1, 2, 3)
遍歷也完全可以這樣 users.forEach(System.out::println);
@Test
public void testSelectByBatchId(){
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
List<User> users = userMapper.selectBatchIds(list);
for (User user : users){
System.out.println(user);
}
}
C:根據 map 查詢
@Test
public void testSelectByMap(){
HashMap<String, Object> map = new HashMap<>();
// 自定義要查詢的欄位和值
map.put("name","理想二旬不止");
map.put("age",30);
List<User> users = userMapper.selectByMap(map);
for (User user : users){
System.out.println(user);
}
}
通過日誌的列印可以看到,它根據我們的選擇自動拼出了 SQL 的條件
==> Preparing: SELECT id,name,age,email,version,create_time,update_time FROM user WHERE name = ? AND age = ?
==> Parameters: 理想二旬不止(String), 30(Integer)
(3) 分頁查詢
JavaWeb 階段,大家都應該有手寫過分頁,配合 SQL 的 limit 進行分頁,後面在 Mybatis 就會用一些例如 pageHelper 的外掛,而 MyBatis-Plus 中也有一個內建的分頁外掛
使用前只需要進行一個小小的配置,在剛才配置類中,加入分頁外掛的配置程式碼
// 掃描我們的mapper 資料夾
@MapperScan("cn.ideal.mapper")
@EnableTransactionManagement
@Configuration // 配置類
public class MyBatisPlusConfig {
// 樂觀鎖外掛
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
// 分頁外掛
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
接著就可以測試分頁了
@Test
public void testPage(){
// Page 引數: 引數1:當前頁 ,引數1:頁面大小
Page<User> page = new Page<>(2,3);
userMapper.selectPage(page,null);
List<User> users = page.getRecords();
for (User user : users){
System.out.println(user);
}
System.out.println(page.getTotal());
}
執行結果日誌:
==> Preparing: SELECT id,name,age,email,version,create_time,update_time FROM user LIMIT ?,?
==> Parameters: 3(Long), 3(Long)
<== Columns: id, name, age, email, version, create_time, update_time
<== Row: 4, Sandy, 21, test4@baomidou.com, 1, null, null
<== Row: 5, Billie, 24, test5@baomidou.com, 1, null, null
<== Row: 1308952901602811906, 理想二旬不止, 30, ideal_bwh@163.com, 1, null, null
<== Total: 3
(4) 條件查詢 ※
如何實現一些條件相對複雜的查詢呢?MyBatis-Plus 也給我們提供了一些用法,幫助我們方便的構造各種條件
其實前面大家應該就注意到了,在查詢操作的可用方法中,引數中往往帶有一個名叫 Wrapper<T> queryWrapper
的內容,這就是我們要構造條件的重點
查詢中最常用的就是 QueryWrapper
說明:
繼承自 AbstractWrapper ,自身的內部屬性 entity 也用於生成 where 條件
及 LambdaQueryWrapper, 可以通過 new QueryWrapper().lambda() 方法獲取
例項化一個 QueryWrapper 後,通過呼叫一些內建的方法,就可以實現條件構造
A:如何使用
例如我們想要構造這樣一個條件:查詢郵箱不為空,且年齡小於 25 歲的使用者
@Test
void contextLoads() {
// 例項化一個 QueryWrapper 物件
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 進行具體條件構造
wrapper
.isNotNull("email")
.lt("age", 25);
// 執行具體的查詢方法,同時將 wrapper 條件作為引數傳入
List<User> users = userMapper.selectList(wrapper);
for (User user : users){
System.out.println(user);
}
}
看一下列印的日誌:
==> Preparing: SELECT id,name,age,email,version,deleted,create_time,update_time FROM user WHERE deleted=0 AND (email IS NOT NULL AND age < ?)
==> Parameters: 25(Integer)
<== Columns: id, name, age, email, version, deleted, create_time, update_time
<== Row: 2, 理想, 22, ideal_bwh@xxx.com, 1, 0, 2020-09-26 15:06:09, 2020-09-26 21:21:52
<== Row: 4, Sandy, 21, test4@baomidou.com, 1, 0, null, null
<== Row: 5, Billie, 24, test5@baomidou.com, 1, 0, null, null
<== Total: 3
可以看到條件都被自動在 SQL 中構造出來了
使用的方式就這麼簡單,通過各種巧妙的構造就好了
B:構造方式
下面是從官網摘取的各種構造方式:
allEq
allEq(Map<R, V> params)
allEq(Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
個別引數說明:
params
: key
為資料庫欄位名,value
為欄位值
null2IsNull
: 為true
則在map
的value
為null
時呼叫 isNull 方法,為false
時則忽略value
為null
的
- 例1:
allEq({id:1,name:"老王",age:null})
--->id = 1 and name = '老王' and age is null
- 例2:
allEq({id:1,name:"老王",age:null}, false)
--->id = 1 and name = '老王'
allEq(BiPredicate<R, V> filter, Map<R, V> params)
allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
個別引數說明:
filter
: 過濾函式,是否允許欄位傳入比對條件中
params
與 null2IsNull
: 同上
- 例1:
allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null})
--->name = '老王' and age is null
- 例2:
allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null}, false)
--->name = '老王'
eq
eq(R column, Object val)
eq(boolean condition, R column, Object val)
- 等於 =
- 例:
eq("name", "老王")
--->name = '老王'
ne
ne(R column, Object val)
ne(boolean condition, R column, Object val)
- 不等於 <>
- 例:
ne("name", "老王")
--->name <> '老王'
gt
gt(R column, Object val)
gt(boolean condition, R column, Object val)
- 大於 >
- 例:
gt("age", 18)
--->age > 18
ge
ge(R column, Object val)
ge(boolean condition, R column, Object val)
- 大於等於 >=
- 例:
ge("age", 18)
--->age >= 18
lt
lt(R column, Object val)
lt(boolean condition, R column, Object val)
- 小於 <
- 例:
lt("age", 18)
--->age < 18
le
le(R column, Object val)
le(boolean condition, R column, Object val)
- 小於等於 <=
- 例:
le("age", 18)
--->age <= 18
between
between(R column, Object val1, Object val2)
between(boolean condition, R column, Object val1, Object val2)
- BETWEEN 值1 AND 值2
- 例:
between("age", 18, 30)
--->age between 18 and 30
notBetween
notBetween(R column, Object val1, Object val2)
notBetween(boolean condition, R column, Object val1, Object val2)
- NOT BETWEEN 值1 AND 值2
- 例:
notBetween("age", 18, 30)
--->age not between 18 and 30
like
like(R column, Object val)
like(boolean condition, R column, Object val)
- LIKE '%值%'
- 例:
like("name", "王")
--->name like '%王%'
notLike
notLike(R column, Object val)
notLike(boolean condition, R column, Object val)
- NOT LIKE '%值%'
- 例:
notLike("name", "王")
--->name not like '%王%'
likeLeft
likeLeft(R column, Object val)
likeLeft(boolean condition, R column, Object val)
- LIKE '%值'
- 例:
likeLeft("name", "王")
--->name like '%王'
likeRight
likeRight(R column, Object val)
likeRight(boolean condition, R column, Object val)
- LIKE '值%'
- 例:
likeRight("name", "王")
--->name like '王%'
isNull
isNull(R column)
isNull(boolean condition, R column)
- 欄位 IS NULL
- 例:
isNull("name")
--->name is null
isNotNull
isNotNull(R column)
isNotNull(boolean condition, R column)
- 欄位 IS NOT NULL
- 例:
isNotNull("name")
--->name is not null
in
in(R column, Collection<?> value)
in(boolean condition, R column, Collection<?> value)
- 欄位 IN (value.get(0), value.get(1), ...)
- 例:
in("age",{1,2,3})
--->age in (1,2,3)
in(R column, Object... values)
in(boolean condition, R column, Object... values)
- 欄位 IN (v0, v1, ...)
- 例:
in("age", 1, 2, 3)
--->age in (1,2,3)
notIn
notIn(R column, Collection<?> value)
notIn(boolean condition, R column, Collection<?> value)
- 欄位 NOT IN (value.get(0), value.get(1), ...)
- 例:
notIn("age",{1,2,3})
--->age not in (1,2,3)
notIn(R column, Object... values)
notIn(boolean condition, R column, Object... values)
- 欄位 NOT IN (v0, v1, ...)
- 例:
notIn("age", 1, 2, 3)
--->age not in (1,2,3)
inSql
inSql(R column, String inValue)
inSql(boolean condition, R column, String inValue)
- 欄位 IN ( sql語句 )
- 例:
inSql("age", "1,2,3,4,5,6")
--->age in (1,2,3,4,5,6)
- 例:
inSql("id", "select id from table where id < 3")
--->id in (select id from table where id < 3)
notInSql
notInSql(R column, String inValue)
notInSql(boolean condition, R column, String inValue)
- 欄位 NOT IN ( sql語句 )
- 例:
notInSql("age", "1,2,3,4,5,6")
--->age not in (1,2,3,4,5,6)
- 例:
notInSql("id", "select id from table where id < 3")
--->id not in (select id from table where id < 3)
groupBy
groupBy(R... columns)
groupBy(boolean condition, R... columns)
- 分組:GROUP BY 欄位, ...
- 例:
groupBy("id", "name")
--->group by id,name
orderByAsc
orderByAsc(R... columns)
orderByAsc(boolean condition, R... columns)
- 排序:ORDER BY 欄位, ... ASC
- 例:
orderByAsc("id", "name")
--->order by id ASC,name ASC
orderByDesc
orderByDesc(R... columns)
orderByDesc(boolean condition, R... columns)
- 排序:ORDER BY 欄位, ... DESC
- 例:
orderByDesc("id", "name")
--->order by id DESC,name DESC
orderBy
orderBy(boolean condition, boolean isAsc, R... columns)
- 排序:ORDER BY 欄位, ...
- 例:
orderBy(true, true, "id", "name")
--->order by id ASC,name ASC
having
having(String sqlHaving, Object... params)
having(boolean condition, String sqlHaving, Object... params)
- HAVING ( sql語句 )
- 例:
having("sum(age) > 10")
--->having sum(age) > 10
- 例:
having("sum(age) > {0}", 11)
--->having sum(age) > 11
func
func(Consumer<Children> consumer)
func(boolean condition, Consumer<Children> consumer)
- func 方法(主要方便在出現if...else下呼叫不同方法能不斷鏈)
- 例:
func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})
or
or()
or(boolean condition)
- 拼接 OR
注意事項:
主動呼叫or
表示緊接著下一個方法不是用and
連線!(不呼叫or
則預設為使用and
連線)
- 例:
eq("id",1).or().eq("name","老王")
--->id = 1 or name = '老王'
or(Consumer<Param> consumer)
or(boolean condition, Consumer<Param> consumer)
- OR 巢狀
- 例:
or(i -> i.eq("name", "李白").ne("status", "活著"))
--->or (name = '李白' and status <> '活著')
and
and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)
- AND 巢狀
- 例:
and(i -> i.eq("name", "李白").ne("status", "活著"))
--->and (name = '李白' and status <> '活著')
nested
nested(Consumer<Param> consumer)
nested(boolean condition, Consumer<Param> consumer)
- 正常巢狀 不帶 AND 或者 OR
- 例:
nested(i -> i.eq("name", "李白").ne("status", "活著"))
--->(name = '李白' and status <> '活著')
apply
apply(String applySql, Object... params)
apply(boolean condition, String applySql, Object... params)
- 拼接 sql
注意事項:
該方法可用於資料庫函式 動態入參的params
對應前面applySql
內部的{index}
部分.這樣是不會有sql注入風險的,反之會有!
- 例:
apply("id = 1")
--->id = 1
- 例:
apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
- 例:
apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08")
--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
last
last(String lastSql)
last(boolean condition, String lastSql)
- 無視優化規則直接拼接到 sql 的最後
注意事項:
只能呼叫一次,多次呼叫以最後一次為準 有sql注入的風險,請謹慎使用
- 例:
last("limit 1")
exists
exists(String existsSql)
exists(boolean condition, String existsSql)
- 拼接 EXISTS ( sql語句 )
- 例:
exists("select id from table where age = 1")
--->exists (select id from table where age = 1)
notExists
notExists(String notExistsSql)
notExists(boolean condition, String notExistsSql)
- 拼接 NOT EXISTS ( sql語句 )
- 例:
notExists("select id from table where age = 1")
--->not exists (select id from table where age = 1)
(四) 刪除操作
(1) 可用方法
// 根據 entity 條件,刪除記錄
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 刪除(根據ID 批量刪除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根據 ID 刪除
int deleteById(Serializable id);
// 根據 columnMap 條件,刪除記錄
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
(2) 簡單刪除
A:根據 id 刪除
@Test
public void testDeleteById(){
userMapper.deleteById(1L);
}
B:根據 id 集合刪除
@Test
public void testDeleteBatchIds(){
List list = new ArrayList();
list.add(1308952901602811906L);
list.add(1308952901602811907L);
list.add(1308952901602811908L);
list.add(1308952901602811909L);
userMapper.deleteBatchIds(list);
}
C:根據 map 刪除
@Test
public void testDeleteMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("name","理想二旬不止");
userMapper.deleteByMap(map);
}
(3) 邏輯刪除
刪除這塊再補充一下邏輯刪除的概念,物理刪除很好理解,就是實實在在的在資料庫中刪沒了,但是邏輯刪除,顧名思義只是邏輯上被刪除了,實際上並沒有,只是通過增加一個欄位讓其失效而已,例如 deleted = 0 => deleted = 1
可以
應用的場景就是管理員想檢視刪除記錄,在錯誤刪除下,可以有逆轉的機會等等
首先資料庫增加 deleted 欄位,同時建立其實體和註解
@TableLogic // 邏輯刪除
private Integer deleted;
接著只需要在全域性配置中配置即可
application.properties
# 配置邏輯刪除
MyBatis-Plus.global-config.db-config.logic-delete-value=1
MyBatis-Plus.global-config.db-config.logic-not-delete-value=0
application.yml
MyBatis-Plus:
global-config:
db-config:
logic-delete-field: flag # 全域性邏輯刪除的實體欄位名(since 3.3.0,配置後可以忽略不配置步驟2)
logic-delete-value: 1 # 邏輯已刪除值(預設為 1)
logic-not-delete-value: 0 # 邏輯未刪除值(預設為 0)
效果如下:
你會發現,邏輯刪除會走一個更新操作,通過修改指定欄位 deleted 的值為 0 實現我們想要的效果
五 程式碼自動生成器
MyBatis-Plus 提供了一個非常便捷,有意思的內容,那就是程式碼的自動生成,我們通過一些配置,就可以自動的生成 controller、service、mapper、pojo 的內容,並且介面或者註解等內容都會按照配置指定的格式生成。(提前準備好資料庫和表)
首先除了 MyBatis-Plus 的依賴以外,還需要引入 swagger 和 velocity 的依賴,但是這兩者其實是可選的,可以選擇不配置就不用引入了,預設使用 velocity 這個模板引擎,大家還可以換成別的
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
例如:
Velocity(預設):
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>latest-velocity-version</version>
</dependency>
Freemarker:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>latest-freemarker-version</version>
</dependency>
Beetl:
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl</artifactId>
<version>latest-beetl-version</version>
</dependency>
-
注意!如果您選擇了非預設引擎,需要在 AutoGenerator 中 設定模板引擎。
AutoGenerator generator = new AutoGenerator(); // set freemarker engine generator.setTemplateEngine(new FreemarkerTemplateEngine()); // set beetl engine generator.setTemplateEngine(new BeetlTemplateEngine()); // set custom engine (reference class is your custom engine class) generator.setTemplateEngine(new CustomTemplateEngine()); // other config ...
下面就是一個主配置了,修改其中的資料庫連線等資訊,以及包的名稱等等等執行就可以了
package cn.ideal;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.ArrayList;
/**
* @ClassName: AutomaticCodeGenerate
* @Description: TODO
* @Author: BWH_Steven
* @Date: 2020/10/2 21:29
* @Version: 1.0
*/
public class AutomaticCodeGenerate {
public static void main(String[] args) {
// 需要構建一個程式碼自動生成器物件
AutoGenerator mpg = new AutoGenerator();
// 配置策略
// 全域性配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setAuthor("BWH_Steven");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setOpen(false);
gc.setFileOverride(false); // 是否覆蓋
gc.setServiceName("%sService"); // 去Service的I字首
gc.setIdType(IdType.ID_WORKER);
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
// 設定資料來源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding" +
"=utf-8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root99");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 包的配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("test");
pc.setParent("cn.ideal");
pc.setEntity("entity");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("user"); // 設定要對映的表名
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true); // 自動lombok;
strategy.setLogicDeleteFieldName("deleted");
// 自動填充配置
TableFill gmtCreate = new TableFill("create_time", FieldFill.INSERT);
TableFill gmtModified = new TableFill("update_time", FieldFill.INSERT_UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(gmtCreate);
tableFills.add(gmtModified);
strategy.setTableFillList(tableFills); // 樂觀鎖
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
mpg.execute(); //執行
}
}
生成結構效果如下:
我簡單貼兩段生成的內容:
controller
package cn.ideal.test.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author BWH_Steven
* @since 2020-10-02
*/
@RestController
@RequestMapping("/test/user")
public class UserController {
}
entity
package cn.ideal.test.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
*
* </p>
*
* @author BWH_Steven
* @since 2020-10-02
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="User物件", description="")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主鍵ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "姓名")
private String name;
@ApiModelProperty(value = "年齡")
private Integer age;
@ApiModelProperty(value = "郵箱")
private String email;
@ApiModelProperty(value = "版本")
@Version
private Integer version;
@TableLogic
private Integer deleted;
@ApiModelProperty(value = "建立時間")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@ApiModelProperty(value = "修改時間")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
service
package cn.ideal.test.service;
import cn.ideal.test.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服務類
* </p>
*
* @author BWH_Steven
* @since 2020-10-02
*/
public interface UserService extends IService<User> {
}
service 實現類
package cn.ideal.test.service.impl;
import cn.ideal.test.entity.User;
import cn.ideal.test.mapper.UserMapper;
import cn.ideal.test.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服務實現類
* </p>
*
* @author BWH_Steven
* @since 2020-10-02
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
mapper
package cn.ideal.test.mapper;
import cn.ideal.test.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 介面
* </p>
*
* @author BWH_Steven
* @since 2020-10-02
*/
public interface UserMapper extends BaseMapper<User> {
}
mapper XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.ideal.test.mapper.UserMapper">
</mapper>
六 結尾
如果文章中有什麼不足,歡迎大家留言交流,感謝朋友們的支援!
如果能幫到你的話,那就來關注我吧!如果您更喜歡微信文章的閱讀方式,可以關注我的公眾號
在這裡的我們素不相識,卻都在為了自己的夢而努力 ❤
一個堅持推送原創開發技術文章的公眾號:理想二旬不止