目錄
一、MyBatis-Plus簡介
二、MyBatisPlus使用的基本流程:
(1)引入MybatisPlus依賴,代替MyBatis依賴
(2)自定義Mapper繼承BaseMapper
?編輯(3)在實體類上添加注解聲明表信息
(4)在application.yml中根據需要添加配置
三、核心功能
1、條件構造器
(1)QueryWrapper
(2)UpdateWrapper
(3)基于Lambda的Wrapper
2、自定義SQL(擅長處理where更新條件)
3、IService接口
4、LambdaQuery和LambdaUpdate
lambdaUpdate實現
5、批量新增 & 批處理方案性能測試
拓展:rewriteBatchedStatements=true 和 allowMultiQueries=true 的區別
@RequiredArgsConstructor注解
四、擴展功能
1、代碼生成
(1)安裝插件
(2)使用步驟
(3)代碼生成器配置
2、靜態工具
3、邏輯刪除
(1)介紹
注意:只有MybatisPlus生成的SQL語句才支持自動的邏輯刪除,自定義SQL需要自己手動處理邏輯刪除。
(2)@TableLogic
4、枚舉處理器
(1)定義枚舉,標記@EnumValue
(2)配置枚舉處理器
5、JSON類型處理器
(1)定義接收Json的實體類
(2)指定類型處理器
6、yaml配置加密
(1)生成密鑰????????
(2)修改配置
(3)配置密鑰運行參數
(4)實現原理
7、自動填充字段
(1)配置自動填充處理器
(2)添加@TableField的fill屬性
五、插件功能
1、分頁插件
(1)引入依賴
(2)配置分頁內置攔截器
(3)分頁API
2、通用分頁實體
(1)實體類設計
(2)開發接口
(3)改造PageDTO實體
(4)改造PageResult實體
一、MyBatis-Plus簡介
MyBatis-Plus 是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
- 支持的數據庫
- 框架結構
MyBatis-Plus官網:https://baomidou.com/
參考文檔:https://mybatis.plus/ (網站訪問速度稍慢,建議直接看官網文檔)
二、MyBatisPlus使用的基本流程:
?
(1)引入MybatisPlus依賴,代替MyBatis依賴
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且實現了自動裝配效果。
<!-- springboot2的mybatis-plus依賴 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version>
</dependency>
注意:如果是springboot3,引入的是mybatis-plus-spring-boot3-starter依賴。
<!-- springboot3的mybatis-plus依賴 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version>
</dependency>
(2)自定義Mapper繼承BaseMapper

(3)在實體類上添加注解聲明表信息
MybatisPlus底層通過反射,根據PO實體的信息來推斷出表的信息,從而生成SQL的。默認情況下(約定):
- MybatisPlus會把PO實體的類名駝峰轉下劃線作為表名
- MybatisPlus會把PO實體的所有變量名駝峰轉下劃線作為表的字段名,并根據變量類型推斷字段類型
- MybatisPlus會把名為id的字段作為主鍵
但很多情況下,默認的實現與實際場景不符(實際情況與MP的約定不符合時使用),因此MybatisPlus提供了一些注解便于我們聲明表信息。
- @TableName:用來指定表名
- @Tableld:用來指定表中的主鍵字段信息
- @TableField:用來指定表中的普通字段信息
常見注解:
(4)在application.yml中根據需要添加配置
常見配置:
三、核心功能
1、條件構造器
除了新增以外,修改、刪除、查詢的SQL語句都需要指定where條件。因此BaseMapper中提供的相關方法除了以id
作為where
條件以外,還支持更加復雜的where
條件。
參數中的Wrapper
就是條件構造的抽象類,其下有很多默認實現,繼承關系如圖:
Wrapper
的子類AbstractWrapper
提供了where中包含的所有條件構造方法:
而QueryWrapper在AbstractWrapper的基礎上拓展了一個select方法,允許指定查詢字段:
而UpdateWrapper在AbstractWrapper的基礎上拓展了一個set方法,允許指定SQL中的SET部分:
(1)QueryWrapper
無論是修改、刪除、查詢,都可以使用QueryWrapper來構建查詢條件。
示例:
// 查詢出名字中帶o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testQueryWrapper() {// 1.構建查詢條件 where username like "%o%" AND balance >= 1000QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2.查詢數據List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}
// 更新用戶名為jack的用戶的余額為2000。
@Test
void testUpdateByQueryWrapper() {// 1.設置要更新的數據User user = new User();user.setBalance(2000);// 2.構建更新條件 where username = "Jack"QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("username", "Jack");// 3.執行更新,user中非null字段都會作為set語句System.out.println(userMapper.update(user, queryWrapper) > 0);
}
(2)UpdateWrapper
基于BaseMapper中的update方法更新時只能直接賦值,對于一些復雜的需求就難以實現。
示例:
@Test
void testUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLUpdateWrapper<User> updateWrapper = new UpdateWrapper<User>().setSql("balance = balance - 200") // SET balance = balance - 200.in("id", ids); // WHERE id in (1, 2, 4)// 2.基于UpdateWrapper中的setSql來更新System.out.println(userMapper.update(updateWrapper) > 0);
}
(3)基于Lambda的Wrapper
LambdaQueryWrapper
,對應QueryWrapperLambdaUpdateWrapper
,對應UpdateWrapper
示例:
@Test
void testLambdaUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLLambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<User>().setSql("balance = balance - 200") // SET balance = balance - 200.in(User::getId, ids); // WHERE id in (1, 2, 4)// 2.基于UpdateWrapper中的setSql來更新System.out.println(userMapper.update(lambdaUpdateWrapper) > 0);
}@Test
void testLambdaQueryWrapper() {// 1.構建查詢條件 where username like "%o%" AND balance >= 1000LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2.查詢數據List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}
總結:
- QueryWrapper和LambdaQueryWrapper通常用來構建select、delete、update的where條件部分
- UpdateWrapper和LambdaUpdateWrapper通常只有在set語句比較特殊的情況才使用
- 盡量使用LambdaQueryWrapper和LambdaUpdateWrapper避免硬編碼
2、自定義SQL(擅長處理where更新條件)
- 問題引出
把Mapper層的sql語句寫在Service層了,這在某些企業也是不允許的,因為SQL語句最好都維護在持久層,而不是業務層。
示例:
@Test
void testCustomSQLUpdate() {// 更新條件List<Long> ids = List.of(1L, 2L, 4L);int amount = 200;// 定義條件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 調用自定義SQL方法userMapper.updateBalanceByIds(wrapper, amount);
}
${ew.customSqlSegment}
:為自定義SQL片段,@Param("ew")
其中參數ew
必須叫這個,如果忘記了也可以用baomidou
包下的常量類Constants.WRAPPER
,其值等于"ew"
。
3、IService接口
MybatisPlus不僅提供了BaseMapper
,還提供了通用的Service接口及默認實現,封裝了一些常用的service模板方法。
繼承+實現
由于Service中經常需要定義與業務有關的自定義方法,因此我們不能直接使用IService,而是自定義Service接口,然后繼承MP的IService接口以拓展方法。讓自定義的ServiceImpl實現類實現自定義的Service接口,同時繼承MP的默認實現類 ServiceImpl,同時,這樣就不用自己實現IService接口中的方法了。
代碼示例:
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;public interface IUserService extends IService<User> {
}import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
注意:ServiceImpl<M, T>接口的泛型參數中,M是繼承了BaseMapper的Mapper接口,T是PO實體類。
4、LambdaQuery和LambdaUpdate
IService中還提供了Lambda功能來簡化我們的復雜查詢及更新功能。
代碼示例:
public interface IUserService extends IService<User> {List<User> queryUserByCondition(UserQueryDTO queryDTO);
}// 基于Lambda查詢
@Override
public List<User> queryUserByCondition(UserQueryDTO queryDTO) {String name = queryDTO.getName();Integer status = queryDTO.getStatus();Integer minBalance = queryDTO.getMinBalance();Integer maxBalance = queryDTO.getMaxBalance();return lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance).list();
}
MP對LambdaQueryWrapper和LambdaUpdateWrapper的用法進一步做了簡化。我們無需自己通過new的方式來創建Wrapper,而是直接調用lambdaQuery和lambdaUpdate方法。在組織查詢條件的時候,我們加入了name != null這樣的參數,意思就是當條件成立時才會添加這個查詢條件,類似Mybatis的mapper.xml文件中的<if>標簽。這樣就實現了動態查詢條件效果了。
MybatisPlus會根據鏈式編程的最后一個方法來判斷最終的返回結果。lambdaQuery方法中除了可以構建條件,還需要在鏈式編程的最后添加一個查詢結果,list()表示查詢結果返回一個List集合。可選的常用方法有:
- one():最多1個結果
- list():返回集合結果
- count():返回計數結果
- exist():返回查詢的結果是否存在
lambdaUpdate實現
代碼示例:
@Override
@Transactional
public void deductBalanceById(Long id, Integer money) {// 查詢用戶User user = getById(id);// 校驗用戶狀態if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶狀態異常!");}// 校驗余額是否充足if (user.getBalance() < money) {throw new RuntimeException("用戶余額不足!");}// 扣減余額//baseMapper.deductBalance(id, money);int remainBalance = user.getBalance() - money;lambdaUpdate().set(User::getBalance, remainBalance) // 更新余額.set(remainBalance == 0, User::getStatus, 2) // 動態判斷是否更新status.eq(User::getBalance, user.getBalance()) // CAS樂觀鎖.eq(User::getId, id) // 根據id扣減對應用戶的余額.update(); // 注意:LambdaUpdate做復雜更新時,最后必須記得加上.update()進行更新操作
}
5、批量新增 & 批處理方案性能測試
需求:批量插入10萬條用戶數據,并作出對比。
- 方式一:普通for循環逐條插入
- 方式二:IService的批量插入(默認不開啟 jdbc 批處理參數)
- 方式三:開啟rewriteBatchedStatements=true參數
方式一:執行結果耗時大約為551.9秒
/*** 10w次插入意味著10w次網絡請求,耗時最慢*/
@Test
void testSaveOneByOne() {long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {userService.save(buildUser(i));}long e = System.currentTimeMillis();System.out.println("耗時:" + (e - b));
}private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123");user.setPhone("" + (18688190000L + i));user.setBalance(2000);user.setInfo("{\"age\": 24, \"intro\": \"英文老師\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(user.getCreateTime());return user;
}
方式二:執行結果耗時大約為27.6秒,打包逐條插入,從網絡請求層面大大減少了耗時。
/*** MP批處理采用的是JDBC底層的預編譯方案PreparedStatement,將1000條數據統一打包執行save一并提交到MySQL,每1000條發送一次網絡請求,插入100次共發送100次網絡請求* MP如果不加JDBC連接參數rewriteBatchedStatements=true,底層還是打包逐條插入,只不過是從網絡請求數量上減少了耗時* 而加上了MySQL的這個開啟批處理參數后,MP調用的JDBC底層的批處理才能真正變成一次性批量插入多條數據*/
@Test
void testSaveBatch() {// 因為一次性new 10萬條數據占用內存太多,并且向數據庫請求的數據包有上限大小限制(一次網絡傳輸的數據量是有限的)// 所以我們每次批量插入1000條件,插入100次即10萬條數據// 準備一個容量為1000的集合List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {// 添加一個userlist.add(buildUser(i));// 每1000條批量插入一次if (i % 1000 == 0) {// 批量插入userService.saveBatch(list);// 清空集合,準備下一批數據list.clear();}}long e = System.currentTimeMillis();System.out.println("耗時:" + (e - b));
}
方式三:開啟參數后,測試耗時大約為6.5秒
利用MP的jdbc批處理,只不過MySQL本身默認沒有開啟這個批處理參數rewriteBatchedStatements=true,該參數在MySQL 3.1.13版本開始引入,默認值為false不開啟。
spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456
批處理方案總結:
- 普通for循環逐條插入速度極差,不推薦
- MP的批量新增,基于預編譯的批處理,性能不錯
- 配置jdbc參數,開啟rewriteBatchedStatements,性能最好
拓展:rewriteBatchedStatements=true 和 allowMultiQueries=true 的區別
- rewriteBatchedStatements是重寫sql語句達到發送一次sql的請求效果;allowMultiQueries是在mapper.xml中使用;分號分隔多條sql,允許多條語句一起發送執行的。
- 對于insert批處理操作,開啟rewriteBatchedStatements=true,驅動則會把多條sql語句重寫成一條sql語句然后再發出去;而對于update和delete批處理操作,開啟allowMultiQueries=true,驅動所做的事就是把多條sql語句累積起來再一次性發出去。
@RequiredArgsConstructor注解
@RequiredArgsConstructor
?是?Project Lombok?庫提供的一個注解,它用于在編譯時自動生成一個包含所有?“必需”字段?的構造函數。哪些字段被認為是“必需的”?
所有被?
final
?關鍵字修飾的字段:這些字段必須在對象創建時被初始化。所有被?
@NonNull
?注解標記且未初始化的字段:這些字段不能為?null
,因此也必須在構造函數中初始化。對于普通的、非 final 且沒有?
@NonNull
?約束的字段,不會被包含在這個生成的構造函數中。
四、擴展功能
1、代碼生成
(1)安裝插件
在idea的plugins市場中搜索并安裝MyBatisPlus
插件:
(2)使用步驟
我們利用插件生成一下。 首先需要配置數據庫地址,在Idea頂部菜單中,找到other
,選擇Config Database
,在彈出的窗口中填寫數據庫連接的基本信息:
點擊OK保存。然后再次點擊Idea頂部菜單中的other,然后選擇Code Generator
,在彈出的表單中填寫信息:
最終,代碼自動生成到指定的位置了
(3)代碼生成器配置
如果不想用圖形化界面方式配置生成代碼,使用MyBatis-Plus官網提供的代碼生成器模板也是可以的,但需要自己填寫配置信息。
因為MP代碼生成更新迭代速度很快,若本文的API被棄用,請以官網最新版本API為準:
MyBatis-Plus新代碼生成器:https://baomidou.com/guides/new-code-generator/
代碼生成器配置:https://baomidou.com/reference/new-code-generator-configuration/
- 引入依賴
<!-- MP代碼生成器 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.9</version>
</dependency>
- 代碼生成模板配置示例
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;import java.util.Collections;
import java.util.List;public class CodeGenerator {/*** 數據庫鏈接地址**/private static final String JDBC_URL_MAN = "jdbc:mysql://xxxxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8";/*** 數據庫登錄賬號**/private static final String JDBC_USER_NAME = "xx";/*** 數據庫登錄密碼**/private static final String JDBC_PASSWORD = "xxxx";public static void main(String[] args) {String dir = "\\xx\\xxx";String tablePrefix = "tb_";List<String> tables = List.of("tb_user");FastAutoGenerator.create(JDBC_URL_MAN, JDBC_USER_NAME, JDBC_PASSWORD).globalConfig(builder -> {builder.author("Aizen") // 作者.outputDir(System.getProperty("user.dir") + dir + "\\src\\main\\java") // 輸出路徑(寫到java目錄).enableSwagger() // 開啟swagger.commentDate("yyyy-MM-dd"); // 設置注釋日期格式,默認值: yyyy-MM-dd}).packageConfig(builder -> {builder.parent("com.{company}") // 設置父包名.moduleName("{model}") // 設置父包模塊名.entity("domain") // 設置實體類包名.service("service") // 設置Service接口包名.serviceImpl("service.impl")// 設置Service實現類包名.controller("controller") // 設置Controller包名.mapper("mapper") // 設置Mapper接口文件包名.xml("mappers") // 設置Mapper XML文件包名.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + dir + "\\src\\main\\resources\\mapper"));}).strategyConfig(builder -> {builder.addInclude(tables) // 設置需要生成的表名.addTablePrefix(tablePrefix) // 設置表前綴.serviceBuilder() // 設置 Service 層模板.formatServiceFileName("%sService").formatServiceImplFileName("%sServiceImpl").entityBuilder() // 設置實體類模板.enableLombok() // 啟用 Lombok.logicDeleteColumnName("deleted") // 邏輯刪除字段名(數據庫字段).enableTableFieldAnnotation() // 開啟生成實體時生成字段注解.controllerBuilder().formatFileName("%sController").enableRestStyle() // 啟用 REST 風格.mapperBuilder() // Mapper 策略配置.enableBaseResultMap() // 生成通用的resultMap.superClass(BaseMapper.class) // 設置父類.formatMapperFileName("%sMapper").enableMapperAnnotation().formatXmlFileName("%sMapper");})//.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默認的是Velocity引擎模板.execute(); // 執行生成}
}
2、靜態工具
有的時候Service之間也會相互調用,為了避免出現循環依賴問題,MybatisPlus提供一個靜態工具類:Db
,其中的一些靜態方法與IService
中方法簽名基本一致,也可以幫助我們實現CRUD功能:
因為靜態方法無法讀取類上的泛型,所以MP在使用靜態工具讀取表信息時,需要傳入PO實體類的Class字節碼,MP再通過反射獲取到表信息。其中新增和修改的方法由于需要傳入實體類對象,因此不用傳入實體類的Class字節碼。
代碼示例:
@Test
void testDbGet() {User user = Db.getById(1L, User.class);System.out.println(user);
}@Test
void testDbList() {// 利用Db實現復雜條件查詢List<User> list = Db.lambdaQuery(User.class).like(User::getUsername, "o").ge(User::getBalance, 1000).list();list.forEach(System.out::println);
}@Test
void testDbUpdate() {Db.lambdaUpdate(User.class).set(User::getBalance, 2000).eq(User::getUsername, "Rose");
}
3、邏輯刪除
(1)介紹
對于一些比較重要的數據,我們往往會采用邏輯刪除的方案,即:
- 在表中添加一個字段標記數據是否被刪除,邏輯刪除字段的屬性通常是
Integer
或Boolean
類型。 - 當刪除數據時把標記置為1,1表示已刪除
- 查詢時只查詢標記為0的數據,0表示未刪除
同理更新操作也需要加上deleted = 0,所以一旦采用了邏輯刪除,所有的查詢、刪除、更新邏輯都要跟著變化,非常麻煩。
為了解決這個問題,MybatisPlus就添加了對邏輯刪除的支持。無需改變方法調用的方式,而是在底層幫我們自動修改CRUD的語句。我們只需要在application.yaml文件中配置邏輯刪除的字段名稱和值即可。
?
注意:只有MybatisPlus生成的SQL語句才支持自動的邏輯刪除,自定義SQL需要自己手動處理邏輯刪除。
代碼示例:
@SpringBootTest
class IAddressServiceTest {@Autowiredprivate IAddressService addressService;@Testvoid testLogicDelete() {// 刪除addressService.removeById(59L);// 查詢Address address = addressService.getById(59L);System.out.println("address = " + address);}
}
(2)@TableLogic
@TableLogic
注解用于標記實體類中的邏輯刪除字段。
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("address")
public class Address implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;// 省略.../*** 邏輯刪除*/@TableLogic // @TableLogic注解用于標記實體類中的邏輯刪除字段//@TableLogic(value = "0", delval = "1") // value表示默認邏輯未刪除值,delval表示默認邏輯刪除值 (這兩個值可無、會自動獲取全局配置)@TableField("deleted")private Boolean deleted;
}
使用這種方式就不用在application.yaml
中配置MP邏輯刪除字段了,直接在邏輯刪除字段屬性上加該注解即可。
總結:邏輯刪除本身也有自己的問題,比如
- 會導致數據庫表垃圾數據越來越多,從而影響查詢效率
- SQL中全都需要對邏輯刪除字段做判斷,影響查詢效率
因此,不太推薦采用邏輯刪除功能,如果數據不能刪除,可以采用把數據遷移到其它表的辦法。
實際業務應該有回收站的邏輯處理數據
4、枚舉處理器
當實體類屬性是枚舉類型,在與數據庫的字段類型做轉換時,底層默認使用的是MyBatis提供的EnumOrdinalTypeHandler
枚舉類型處理器。
但是這個并不好用,所以MP對類型處理器做了增強,其中增強后的枚舉處理器叫MybatisEnumTypeHandler
,JSON處理器叫AbstractJsonTypeHandler
。
(1)定義枚舉,標記@EnumValue
代碼示例:
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "凍結");@EnumValueprivate final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
要讓MybatisPlus
處理枚舉與數據庫類型自動轉換,我們必須告訴MybatisPlus
,枚舉中的哪個字段的值作為數據庫值。?MybatisPlus
提供了@EnumValue
注解來標記枚舉屬性值。因此我們需要給枚舉中與數據庫字段類型對應的屬性值添加@EnumValue
注解。
(2)配置枚舉處理器
在application.yaml文件中配置枚舉處理器
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
如果我們想向前端返回指定的枚舉屬性,例如value狀態值或desc描述,SpringMVC負責處理響應數據,它在底層處理Json時用的是jackson,所以我們只需要使用jackson提供的@JsonValue
注解,來標記JSON序列化后展示的字段。
代碼示例:
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "凍結");@EnumValueprivate final int value;@JsonValueprivate final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
效果:
5、JSON類型處理器
數據庫的user表中有一個info
字段,是JSON類型
這樣一來,我們要讀取info中的屬性時就非常不方便。如果要方便獲取,info的類型最好是一個Map
或者實體類。而一旦我們把info
改為對象
類型,就需要在寫入數據庫時手動轉為String
,再讀取數據庫時,手動轉換為對象
,這會非常麻煩。
因此MybatisPlus提供了很多特殊類型字段的類型處理器,解決特殊字段類型與數據庫類型轉換的問題。例如處理JSON就可以使用JacksonTypeHandler
處理器(SpringMVC底層默認也是使用的這個類)。
(1)定義接收Json的實體類
首先定義一個單獨實體類UserInfo
來與info字段的屬性匹配
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of") // 為了方便構建對象,為有參構造提供靜態方法,名為of,UserInfo.of()
public class UserInfo {private Integer age;private String intro;private String gender;
}
(2)指定類型處理器
將User和UserVO類的info字段修改為UserInfo類型,并聲明類型處理器@TableField(typeHandler = JacksonTypeHandler.class)。另外,將info改為對象類型后出現對象嵌套,在復雜嵌套查詢時需要使用resultMap結果集映射,否則無法映射。所以還需要再@TableName注解中添加autoResultMap=true確保能夠正常映射。
/*** 詳細信息*/@TableField(typeHandler = JacksonTypeHandler.class)private UserInfo info;
如果啟動mapper.xml報錯,在info字段后加上
, typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler
就解決了。
效果:
6、yaml配置加密
目前我們配置文件中的很多參數都是明文,如果開發人員發生流動,很容易導致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。
以數據庫的用戶名和密碼為例:
(1)生成密鑰????????
首先,我們利用MP提供的AES工具生成一個隨機秘鑰,然后對用戶名、密碼加密。
import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class MpDemoApplicationTest {public static final String USERNAME = "root";public static final String PASSWORD = "123456";@Testvoid testEncrypt() {// 生成 16 位隨機 AES 密鑰String randomKey = AES.generateRandomKey();System.out.println("randomKey = " + randomKey); // randomKey = 7pSEa6F9TnYacTNJ// 利用密鑰對用戶名加密String username = AES.encrypt(USERNAME, randomKey);System.out.println("username = " + username); // username = O4Yq+WKYGlPW5t8QvgrhUQ==// 利用密鑰對用戶名加密String password = AES.encrypt(PASSWORD, randomKey);System.out.println("password = " + password); // password = cDYHnWysq07zUIAy1tcbRQ==}
}
(2)修改配置
修改application.yaml文件,把jdbc的用戶名、密碼修改為剛剛加密生成的密文。
spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: mpw:O4Yq+WKYGlPW5t8QvgrhUQ== # 密文要以 mpw:開頭password: mpw:cDYHnWysq07zUIAy1tcbRQ== # 密文要以 mpw:開頭
(3)配置密鑰運行參數
在啟動項目的時候,需要把剛才生成的AES秘鑰添加到Jar啟動參數
中,像這樣:
--mpw.key=7pSEa6F9TnYacTNJ
,新版本idea添加Program arguments
中設置,界面如下
單元測試的時候不能添加啟動參數,所以要在測試類的注解上配置:@SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ")
(4)實現原理
SpringBoot提供修改Spring環境后置處理器【EnvironmentPostProcessor】,允許在應用程序之前操作環境屬性值,MyBatisPlus對其進行了重寫實現。
package com.baomidou.mybatisplus.autoconfigure;import com.baomidou.mybatisplus.core.toolkit.AES;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.SimpleCommandLinePropertySource;import java.util.HashMap;/*** 安全加密處理器** @author hubin* @since 2020-05-23*/
public class SafetyEncryptProcessor implements EnvironmentPostProcessor {@Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {/*** 命令行中獲取密鑰*/String mpwKey = null;for (PropertySource<?> ps : environment.getPropertySources()) {if (ps instanceof SimpleCommandLinePropertySource) {SimpleCommandLinePropertySource source = (SimpleCommandLinePropertySource) ps;mpwKey = source.getProperty("mpw.key");break;}}/*** 處理加密內容*/if (StringUtils.isNotBlank(mpwKey)) {HashMap<String, Object> map = new HashMap<>();for (PropertySource<?> ps : environment.getPropertySources()) {if (ps instanceof OriginTrackedMapPropertySource) {OriginTrackedMapPropertySource source = (OriginTrackedMapPropertySource) ps;for (String name : source.getPropertyNames()) {Object value = source.getProperty(name);if (value instanceof String) {String str = (String) value;if (str.startsWith("mpw:")) {map.put(name, AES.decrypt(str.substring(4), mpwKey));}}}}}// 將解密的數據放入環境變量,并處于第一優先級上if (CollectionUtils.isNotEmpty(map)) {environment.getPropertySources().addFirst(new MapPropertySource("custom-encrypt", map));}}}
}
7、自動填充字段
MyBatis-Plus提供了一個便捷的自動填充功能,用于在插入或更新數據時自動填充某些字段,如創建時間、更新時間等。
(1)配置自動填充處理器
自動填充功能通過實現 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 接口來實現。我們需要創建一個類來實現這個接口,并在其中定義插入和更新時的填充邏輯。添加@Component配置自動填充處理器類被Spring管理。
MyMetaObjectHandler
實現MetaObjectHandler
接口
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {log.info("開始插入填充...");// 起始版本 3.3.3(推薦)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// Date類型填充//this.strictInsertFill(metaObject, "createTime", () -> new Date(), Date.class);// 起始版本 3.3.0(推薦使用)//this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// 也可以使用(3.3.0 該方法有bug)//this.fillStrategy(metaObject, "createTime", LocalDateTime.now());}@Overridepublic void updateFill(MetaObject metaObject) {log.info("開始更新填充...");// 起始版本 3.3.3(推薦)this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// Date類型填充//this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class);// 起始版本 3.3.0(推薦)//this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 也可以使用(3.3.0 該方法有bug)//this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 該方法有bug)}
}
(2)添加@TableField的fill屬性
在實體類中,你需要使用?@TableField
?注解來標記哪些字段需要自動填充,并通過fill
屬性指定填充的策略。
@Data
@TableName(value = "user", autoResultMap = true)
public class User {// 省略.../*** 創建時間*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 更新時間*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
注意事項:
- 自動填充是直接給實體類的屬性設置值,如果屬性沒有值,入庫時會是null。
- MetaObjectHandler 提供的默認方法策略是:如果屬性有值則不覆蓋,如果填充值為 null 則不填充。
- 字段必須聲明 @TableField 注解,并設置 fill 屬性來選擇填充策略。
- 在 update(T entity, Wrapper<T> updateWrapper) 時,entity 不能為空,否則自動填充失效。
- 在 update(Wrapper<T> updateWrapper) 時不會自動填充,需要手動賦值字段條件。
- 使用 strictInsertFill 或 strictUpdateFill 方法可以根據注解 FieldFill.xxx、字段名和字段類型來區分填充邏輯。如果不需區分,可以使用 fillStrategy 方法。
五、插件功能
MybatisPlus提供了很多的插件功能,進一步拓展其功能。目前已有的插件有:
- PaginationInnerInterceptor:自動分頁
- TenantLineInnerInterceptor:多租戶
- DynamicTableNameInnerInterceptor:動態表名
- OptimisticLockerInnerInterceptor:樂觀鎖
- IllegalSQLInnerInterceptor:sql 性能規范
- BlockAttackInnerInterceptor:防止全表更新與刪除
注意:使用多個分頁插件的時候需要注意插件定義順序,建議使用順序如下:
- 多租戶,動態表名
- 分頁,樂觀鎖
- sql 性能規范,防止全表更新與刪除
1、分頁插件
在未引入分頁插件的情況下,MybatisPlus
是不支持分頁功能的,IService
和BaseMapper
中的分頁方法都無法正常起效。 所以,我們必須配置分頁插件。
(1)引入依賴
? 注意,MyBatisPlus于 v3.5.9 起,
PaginationInnerInterceptor
已分離出來。如需使用,則需單獨引入mybatis-plus-jsqlparser
依賴!
<!-- MP分頁插件 jdk 11+ 引入可選模塊 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.9</version>
</dependency>
<!-- MP分頁插件 jdk 8+ 引入可選模塊 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>
(2)配置分頁內置攔截器
新建一個配置類MyBatisConfig
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** MyBatis配置類*/
@Configuration
public class MybatisConfig {/*** MP攔截器*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分頁插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(1000L); // 設置單頁分頁條數最大限制interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}
(3)分頁API
代碼示例:
@Test
void testPageQuery() {// 準備分頁條件int pageNo = 1, pageSize = 2; // 頁碼、每頁查詢條數// 查詢條件:無// 分頁條件Page<User> page = Page.of(pageNo, pageSize);// 排序條件//page.addOrder(new OrderItem("id", true)); // MP老版本排序條件寫法,true為升序,false為降序page.addOrder(OrderItem.desc("balance")); // 新版MP直接用OrderItem的靜態方法page.addOrder(OrderItem.asc("id")); // 可以添加多個排序條件// 分頁查詢page = userService.page(page); // 這里返回的page對象其實和上面是同一個地址,只不過是封裝好分頁查詢結果的page對象// 獲取page中的查詢結果long total = page.getTotal(); // 總條數System.out.println("total = " + total);long pages = page.getPages(); // 總頁數System.out.println("pages = " + pages);List<User> records = page.getRecords(); // 分頁數據records.forEach(System.out::println);
}
效果:
2、通用分頁實體
現在要實現一個用戶分頁查詢的接口,接口規范如下:
這里需要用到4個實體:
- PageDTO:通用分頁查詢條件的實體,包含分頁頁碼、每頁查詢條數、排序字段、是否升序
- UserQueryDTO:接收用戶查詢條件實體,為了實現分頁功能去繼承PageDTO
- PageResult:分頁結果實體,包含總條數、總頁數、當前頁數據
- UserVO:響應用戶頁面視圖實體,將用戶VO集合封裝到PageResult中返回
(1)實體類設計
分頁條件不僅僅用戶分頁查詢需要,以后其它業務也都有分頁查詢的需求。因此建議將分頁查詢條件單獨定義為一個PageDTO
實體
- PageDTO 通用分頁查詢條件的實體
@Data
@ApiModel(description = "通用分頁查詢實體")
public class PageDTO {@ApiModelProperty("頁碼")private Integer pageNo;@ApiModelProperty("每頁查詢條數")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;
}
- UserQueryDTO 接收用戶查詢條件實體,繼承PageDTO
@EqualsAndHashCode(callSuper = true) // 當判斷相等時先考慮父類屬性再考慮子類屬性,就是分頁的時候把分頁條件作為數據請求的的前提,然后再考慮查到了哪些匹配的數據
@Data
@ApiModel(description = "用戶查詢條件實體")
public class UserQueryDTO extends PageDTO {@ApiModelProperty("用戶名關鍵字")private String name;@ApiModelProperty("用戶狀態:1-正常,2-凍結")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
- PageResult 分頁結果實體
@Data
@ApiModel(description = "分頁結果")
public class PageResult<T> {@ApiModelProperty("總條數")private Long total;@ApiModelProperty("總頁數")private Long pages;@ApiModelProperty("結果集合")private List<T> list;
}
- UserVO 響應用戶頁面視圖實體,將用戶VO集合封裝到PageResult中返回,之前已經定義過了
@Data
@ApiModel(description = "用戶VO實體")
public class UserVO {@ApiModelProperty("用戶id")private Long id;@ApiModelProperty("用戶名")private String username;@ApiModelProperty("詳細信息")private UserInfo info;@ApiModelProperty("使用狀態(1正常 2凍結)")private UserStatus status;@ApiModelProperty("賬戶余額")private Integer balance;@ApiModelProperty("用戶的收獲地址")private List<AddressVO> addresses;
}
(2)開發接口
Controller層:UserController
// in UserController
@ApiOperation("根據條件分頁查詢用戶接口")
@GetMapping("/condition/page")
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {return userService.queryUserByConditionAndPage(queryDTO);
}
Service層:IUserService
// in IUserService
PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO);// in UserServiceImpl
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 構建分頁條件Page<User> page = Page.of(queryDTO.getPageNo(), queryDTO.getPageSize());// 構建排序條件if (StrUtil.isNotBlank(queryDTO.getSortBy())) { // 如果排序字段不為空page.addOrder(new OrderItem().setColumn(queryDTO.getSortBy()).setAsc(queryDTO.getIsAsc()));}else { // 如果排序字段為空,默認按照更新時間排序page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));}// 分頁查詢String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// 封裝VO結果PageResult<UserVO> result = new PageResult<>();result.setTotal(p.getTotal()); // 總條數result.setPages(p.getPages()); // 總頁數// 當前頁數據List<User> records = p.getRecords();// 其實也可以不用判斷,因為如果查到的是空集合,轉換完還是空集合,不影響最后的結果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 將用戶集合拷貝為用戶VO集合List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);result.setList(userVOList);return result;
}
(3)改造PageDTO實體
在剛才的代碼中,從PageDTO
到MybatisPlus
的Page
之間轉換的過程還是比較麻煩的。
對于PageDTO構建為MP的分頁對象的部分,我們完全可以在PageDTO
這個實體內部中定義一個轉換方法,簡化開發。
一般是封裝成工具類進行操作
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "通用分頁查詢實體")
public class PageDTO {@ApiModelProperty("頁碼")private Integer pageNo;@ApiModelProperty("每頁查詢條數")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;/*** 在PageDTO內部將PageDTO構建為MP的分頁對象,并設置排序條件* @param items 排序條件(可以有一個或多個)* @return MP的分頁對象* @param <T> MP的分頁對象的泛型*/public <T> Page<T> toMpPage(OrderItem... items) {// 構建分頁條件Page<T> page = Page.of(pageNo, pageSize);// 構建排序條件if (sortBy != null && sortBy.trim().length() > 0) { // 如果排序字段不為空page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));}else if (items != null) { // 如果排序字段為空,且傳入的默認OrderItem不為空,按照調用者傳入的默認OrderItem排序page.addOrder(items);}return page;}/*** 將PageDTO構建為MP的分頁對象* 如果調用者不想new OrderItem對象,可以調用該方法,傳入默認的排序字段和排序規則即可* @param defaultSortBy 排序字段* @param defaultAsc 排序規則,true為asc,false為desc* @return MP的分頁對象* @param <T> MP的分頁對象的泛型*/public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {return toMpPage(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));}/*** 將PageDTO構建為MP的分頁對象,排序條件按update_time更新時間降序* @return MP的分頁對象* @param <T> MP的分頁對象的泛型*/public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {return toMpPage("update_time", false);}/*** 將PageDTO構建為MP的分頁對象,排序條件按create_time創建時間降序* @return MP的分頁對象* @param <T> MP的分頁對象的泛型*/public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {return toMpPage("create_time", false);}
}
(4)改造PageResult實體
在查詢出分頁結果后,數據的非空校驗,數據的VO轉換都是模板代碼,編寫起來很麻煩。
我們完全可以將?PO分頁對象轉換為VO分頁結果對象?的邏輯,封裝到?PageResult
?的內部方法中,簡化整個過程。
相當于定義通用業務工具類
@Data
@ApiModel(description = "分頁結果")
public class PageResult<T> {@ApiModelProperty("總條數")private Long total;@ApiModelProperty("總頁數")private Long pages;@ApiModelProperty("結果集合")private List<T> list;/*** 將PO分頁對象轉換為VO分頁結果對象* @param page PO分頁對象* @param clazz 目標VO的字節碼對象* @return VO分頁結果對象* @param <PO> PO實體* @param <VO> VO實體*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Class<VO> clazz) {// PO分頁對象封裝為VO結果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal()); // 總條數result.setPages(page.getPages()); // 總頁數List<PO> records = page.getRecords(); // 當前頁數據// 其實也可以不用判斷,因為如果查到的是空集合,轉換完還是空集合,不影響最后的結果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 將PO集合拷貝為VO集合List<VO> userVOList = BeanUtil.copyToList(records, clazz);result.setList(userVOList);return result;}/*** 將PO分頁對象轉換為VO分頁結果對象* @param page PO分頁對象* @param convertor 自定義規則轉換器* @return VO分頁結果對象* @param <PO> PO實體* @param <VO> VO實體*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Function<PO, VO> convertor) {// PO分頁對象封裝為VO結果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal()); // 總條數result.setPages(page.getPages()); // 總頁數List<PO> records = page.getRecords(); // 當前頁數據// 其實也可以不用判斷,因為如果查到的是空集合,轉換完還是空集合,不影響最后的結果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 將PO集合轉換為VO集合,轉換動作由調用者來傳遞List<VO> voList = records.stream().map(convertor).collect(Collectors.toList());result.setList(voList);return result;}
}
最終Service層代碼:
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 構建分頁條件對象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分頁查詢String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// PO分頁對象封裝為VO結果return PageResult.of(p, UserVO.class);
}
如果是希望自定義PO到VO的轉換過程,可以調用重載方法of(Page<PO> page, Function<PO, VO> convertor)
,convertor的轉換器邏輯由調用者去編寫傳遞:
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 構建分頁條件對象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分頁查詢String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// PO分頁對象封裝為VO結果return PageResult.of(p, user -> {// PO拷貝基礎屬性得到VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 對VO進行處理特殊邏輯String username = userVO.getUsername();// 例如用戶名脫敏處理userVO.setUsername(username.length() > 2 ? StrUtil.fillAfter(username.substring(0, 2), '*', username.length()) : username.charAt(0) + "*");return userVO;});
}
自定義轉換規則的場景,例如:
- ① PO字段和VO字段不是包含關系,出現字段不一致。
- ② 對VO中的屬性做一些過濾、數據脫敏、加密等操作。
- ③ 將VO中的屬性繼續設置數據,例如VO中的address屬性,可以查詢出用戶所屬的收獲地址,設置后一并返回。
效果: