中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁(yè) > news >正文

網(wǎng)站建設(shè)優(yōu)化服務(wù)如何鳳凰軍事新聞最新消息

網(wǎng)站建設(shè)優(yōu)化服務(wù)如何,鳳凰軍事新聞最新消息,wordpress用什么編寫(xiě)的,軟件匯文章目錄 一、MyBatis-Plus簡(jiǎn)介二、快速入門(mén)1、環(huán)境準(zhǔn)備2、將mybatis項(xiàng)目改造成mybatis-plus項(xiàng)目(1)引入MybatisPlus依賴(lài),代替MyBatis依賴(lài)(2)配置Mapper包掃描路徑(3)定義Mapper接口并繼承BaseM…

文章目錄

  • 一、MyBatis-Plus簡(jiǎn)介
  • 二、快速入門(mén)
    • 1、環(huán)境準(zhǔn)備
    • 2、將mybatis項(xiàng)目改造成mybatis-plus項(xiàng)目
      • (1)引入MybatisPlus依賴(lài),代替MyBatis依賴(lài)
      • (2)配置Mapper包掃描路徑
      • (3)定義Mapper接口并繼承BaseMapper<T>
    • 3、常見(jiàn)注解
      • (1)@TableName
      • (2)@TableId
      • (3)@TableField
      • (4)使用案例
    • 4、常見(jiàn)配置
  • 三、核心功能
    • 1、條件構(gòu)造器
      • (1)QueryWrapper
      • (2)UpdateWrapper
      • (3)基于Lambda的Wrapper
    • 2、自定義SQL
      • (1)自定義SQL片段
      • (2)多表聯(lián)查
    • 3、IService接口
      • (1)常用方法介紹
      • (2)基本用法
      • (3)Restful案例
      • (4)LambdaQuery和LambdaUpdate
      • (5)批量新增 & 批處理方案性能測(cè)試
  • 四、擴(kuò)展功能
    • 1、代碼生成
      • (1)安裝插件
      • (2)使用步驟
      • (3)代碼生成器配置
    • 2、靜態(tài)工具
      • (1)案例一
      • (2)案例二
      • (3)案例三
    • 3、邏輯刪除
      • (1)介紹
      • (2)使用步驟
      • (3)@TableLogic
    • 4、枚舉處理器
      • (1)定義枚舉,標(biāo)記@EnumValue
      • (2)配置枚舉處理器
    • 5、JSON類(lèi)型處理器
      • (1)定義接收J(rèn)son的實(shí)體類(lèi)
      • (2)指定類(lèi)型處理器
    • 6、yaml配置加密
      • (1)生成密鑰
      • (2)修改配置
      • (3)配置密鑰運(yùn)行參數(shù)
      • (4)實(shí)現(xiàn)原理
    • 7、自動(dòng)填充字段
      • (1)配置自動(dòng)填充處理器
      • (2)添加@TableField的fill屬性
  • 五、插件功能
    • 1、分頁(yè)插件
      • (1)引入依賴(lài)
      • (2)配置分頁(yè)內(nèi)置攔截器
      • (3)分頁(yè)API
    • 2、通用分頁(yè)實(shí)體
      • (1)實(shí)體類(lèi)設(shè)計(jì)
      • (2)開(kāi)發(fā)接口
      • (3)改造PageDTO實(shí)體
      • (4)改造PageResult實(shí)體




一、MyBatis-Plus簡(jiǎn)介

MyBatis-Plus 是一個(gè) MyBatis 的增強(qiáng)工具,在 MyBatis 的基礎(chǔ)上只做增強(qiáng)不做改變,為簡(jiǎn)化開(kāi)發(fā)、提高效率而生。

  • 支持的數(shù)據(jù)庫(kù)

  • 框架結(jié)構(gòu)

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

參考文檔:https://mybatis.plus/ (網(wǎng)站訪問(wèn)速度稍慢,建議直接看官網(wǎng)文檔)




二、快速入門(mén)

1、環(huán)境準(zhǔn)備

  • 導(dǎo)入數(shù)據(jù)庫(kù)表結(jié)構(gòu)mp.sql,一共三張表useraddress,還有測(cè)試MP注解的表tb_user。
-- --------------------------------------------------------
-- 主機(jī):                           127.0.0.1
-- 服務(wù)器版本:                        8.0.28 - MySQL Community Server - GPL
-- 服務(wù)器操作系統(tǒng):                      Win64
-- HeidiSQL 版本:                  12.2.0.6576
-- --------------------------------------------------------/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;-- 導(dǎo)出 mp 的數(shù)據(jù)庫(kù)結(jié)構(gòu)
CREATE DATABASE IF NOT EXISTS `mp` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `mp`;-- 導(dǎo)出  表 mp.address 結(jié)構(gòu)
CREATE TABLE IF NOT EXISTS `address` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` bigint DEFAULT NULL COMMENT '用戶(hù)ID',`province` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '省',`city` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '市',`town` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '縣/區(qū)',`mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手機(jī)',`street` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '詳細(xì)地址',`contact` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '聯(lián)系人',`is_default` bit(1) DEFAULT b'0' COMMENT '是否是默認(rèn) 1默認(rèn) 0否',`notes` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '備注',`deleted` bit(1) DEFAULT b'0' COMMENT '邏輯刪除',PRIMARY KEY (`id`) USING BTREE,KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;-- 正在導(dǎo)出表  mp.address 的數(shù)據(jù):~11 rows (大約)INSERT INTO `address` (`id`, `user_id`, `province`, `city`, `town`, `mobile`, `street`, `contact`, `is_default`, `notes`, `deleted`) VALUES(59, 2, '北京', '北京', '朝陽(yáng)區(qū)', '13900112222', '金燕龍辦公樓', 'Rose', b'1', NULL, b'0'),(60, 1, '北京', '北京', '朝陽(yáng)區(qū)', '13700221122', '修正大廈', 'Jack', b'0', NULL, b'0'),(61, 1, '上海', '上海', '浦東新區(qū)', '13301212233', '航頭鎮(zhèn)航頭路', 'Jack', b'1', NULL, b'0'),(63, 2, '廣東', '佛山', '永春', '13301212233', '永春武館', 'Rose', b'0', NULL, b'0'),(64, 3, '浙江', '杭州', '拱墅區(qū)', '13567809102', '浙江大學(xué)', 'Hope', b'1', NULL, b'0'),(65, 3, '浙江', '杭州', '拱墅區(qū)', '13967589201', '左岸花園', 'Hope', b'0', NULL, b'0'),(66, 4, '湖北', '武漢', '漢口', '13967519202', '天天花園', 'Thomas', b'1', NULL, b'0'),(67, 3, '浙江', '杭州', '拱墅區(qū)', '13967589201', '左岸花園', 'Hopey', b'0', NULL, b'0'),(68, 4, '湖北', '武漢', '漢口', '13967519202', '天天花園', 'Thomas', b'1', NULL, b'0'),(69, 3, '浙江', '杭州', '拱墅區(qū)', '13967589201', '左岸花園', 'Hopey', b'0', NULL, b'0'),(70, 4, '湖北', '武漢', '漢口', '13967519202', '天天花園', 'Thomas', b'1', NULL, b'0');-- 導(dǎo)出  表 mp.user 結(jié)構(gòu)
CREATE TABLE `user` (`id` BIGINT(19) NOT NULL AUTO_INCREMENT COMMENT '用戶(hù)id',`username` VARCHAR(50) NOT NULL COMMENT '用戶(hù)名' COLLATE 'utf8_general_ci',`password` VARCHAR(128) NOT NULL COMMENT '密碼' COLLATE 'utf8_general_ci',`phone` VARCHAR(20) NULL DEFAULT NULL COMMENT '注冊(cè)手機(jī)號(hào)' COLLATE 'utf8_general_ci',`info` JSON NOT NULL COMMENT '詳細(xì)信息',`status` INT(10) NULL DEFAULT '1' COMMENT '使用狀態(tài)(1正常 2凍結(jié))',`balance` INT(10) NULL DEFAULT NULL COMMENT '賬戶(hù)余額',`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時(shí)間',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `username` (`username`) USING BTREE
)
COMMENT='用戶(hù)表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
ROW_FORMAT=COMPACT
AUTO_INCREMENT=5
;-- 正在導(dǎo)出表  mp.user 的數(shù)據(jù):~4 rows (大約)INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `status`, `balance`, `create_time`, `update_time`) VALUES(1, 'Jack', '123', '13900112224', '{"age": 20, "intro": "佛系青年", "gender": "male"}', 1, 1600, '2023-05-19 20:50:21', '2023-06-19 20:50:21'),(2, 'Rose', '123', '13900112223', '{"age": 19, "intro": "青澀少女", "gender": "female"}', 1, 600, '2023-05-19 21:00:23', '2023-06-19 21:00:23'),(3, 'Hope', '123', '13900112222', '{"age": 25, "intro": "上進(jìn)青年", "gender": "male"}', 1, 100000, '2023-06-19 22:37:44', '2023-06-19 22:37:44'),(4, 'Thomas', '123', '17701265258', '{"age": 29, "intro": "伏地魔", "gender": "male"}', 1, 800, '2023-06-19 23:44:45', '2023-06-19 23:44:45');/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;create table tb_user
(user_id    bigint auto_increment comment '用戶(hù)id',username   varchar(20)       null comment '用戶(hù)名',password   varchar(20)       null comment '密碼',is_deleted TINYINT default 0 null comment '邏輯刪除',`order`    TINYINT           null comment '排序序號(hào)',constraint tb_user_pkprimary key (user_id),constraint tb_user_pk2unique (username)
)
comment '用戶(hù)表(測(cè)試mp注解)';

數(shù)據(jù)庫(kù)表結(jié)構(gòu)如下:

  • 導(dǎo)入項(xiàng)目結(jié)構(gòu)mp-demo

application.yaml中修改jdbc參數(shù)為自己的數(shù)據(jù)庫(kù)參數(shù):

spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456
logging:level:com.itheima: debugpattern:dateformat: HH:mm:ss
mybatis:mapper-locations: classpath*:mapper/*.xml

2、將mybatis項(xiàng)目改造成mybatis-plus項(xiàng)目

基于現(xiàn)有的mybatis項(xiàng)目將其改造成mybatis-plus實(shí)現(xiàn)如下功能:

  1. 新增用戶(hù)功能
  2. 根據(jù)id查詢(xún)用戶(hù)
  3. 根據(jù)id批量查詢(xún)用戶(hù)
  4. 根據(jù)id更新用戶(hù)
  5. 根據(jù)id刪除用戶(hù)

比如我們要實(shí)現(xiàn)User表的CRUD,只需要下面幾步。

(1)引入MybatisPlus依賴(lài),代替MyBatis依賴(lài)

MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且實(shí)現(xiàn)了自動(dòng)裝配效果。

<!-- springboot2的mybatis-plus依賴(lài) -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version>
</dependency>

注意:如果是springboot3,引入的是mybatis-plus-spring-boot3-starter依賴(lài)。

<!-- springboot3的mybatis-plus依賴(lài) -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version>
</dependency>

由于這個(gè)starter包含對(duì)mybatis的自動(dòng)裝配,因此完全可以替換掉Mybatis的starter。 最終,項(xiàng)目的依賴(lài)如下:

<!-- mysql -->
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
<!-- 單元測(cè)試 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version>
</dependency>

(2)配置Mapper包掃描路徑

  • SpringBoot啟動(dòng)類(lèi)上添加@MapperScan注解
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.itheima.mp.mapper")
@SpringBootApplication
public class MpDemoApplication {public static void main(String[] args) {SpringApplication.run(MpDemoApplication.class, args);}
}

(3)定義Mapper接口并繼承BaseMapper

  • 升級(jí)前的MyBatis版本增刪改查

之前MyBatis的Mapper接口:

public interface UserMapper {void saveUser(User user);void deleteUser(Long id);void updateUser(User user);User queryUserById(@Param("id") Long id);List<User> queryUserByIds(@Param("ids") List<Long> ids);
}

之前MyBatis的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="com.itheima.mp.mapper.UserMapper"><!-- 新增用戶(hù) --><insert id="saveUser" parameterType="com.itheima.mp.domain.po.User">INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `balance`)VALUES(#{id}, #{username}, #{password}, #{phone}, #{info}, #{balance});</insert><!-- 修改用戶(hù) --><update id="updateUser" parameterType="com.itheima.mp.domain.po.User">UPDATE `user`<set><if test="username != null">`username`=#{username}</if><if test="password != null">`password`=#{password}</if><if test="phone != null">`phone`=#{phone}</if><if test="info != null">`info`=#{info}</if><if test="status != null">`status`=#{status}</if><if test="balance != null">`balance`=#{balance}</if></set>WHERE `id`=#{id};</update><!-- 刪除用戶(hù) --><delete id="deleteUser" parameterType="com.itheima.mp.domain.po.User">DELETE FROM user WHERE id = #{id}</delete><!-- 根據(jù)用戶(hù)id查詢(xún)單個(gè)用戶(hù) --><select id="queryUserById" resultType="com.itheima.mp.domain.po.User">SELECT *FROM userWHERE id = #{id}</select><!-- 根據(jù)用戶(hù)id數(shù)組批量查詢(xún)多個(gè)用戶(hù) --><select id="queryUserByIds" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user<if test="ids != null">WHERE id IN<foreach collection="ids" open="(" close=")" item="id" separator=",">#{id}</foreach></if>LIMIT 10</select>
</mapper>

之前MyBatis的CRUD測(cè)試類(lèi):

@SpringBootTest
class MyBatisUserMapperTests {@Autowiredprivate UserMapper userMapper;@Testvoid testInsert() {User user = new User();user.setId(5L);user.setUsername("Lucy");user.setPassword("123");user.setPhone("18688990011");user.setBalance(200);user.setInfo("{\"age\": 24, \"intro\": \"英文老師\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());userMapper.saveUser(user);}@Testvoid testSelectById() {User user = userMapper.queryUserById(5L);System.out.println("user = " + user);}@Testvoid testQueryByIds() {List<User> users = userMapper.queryUserByIds(List.of(1L, 2L, 3L, 4L));users.forEach(System.out::println);}@Testvoid testUpdateById() {User user = new User();user.setId(5L);user.setBalance(20000);userMapper.updateUser(user);}@Testvoid testDeleteUser() {userMapper.deleteUser(5L);}
}
  • 升級(jí)后的MyBatisPlus版本的增刪改查

為了簡(jiǎn)化單表CRUD,MybatisPlus提供了一個(gè)基礎(chǔ)的BaseMapper接口,其中已經(jīng)實(shí)現(xiàn)了單表的CRUD:

MyBatisPlus的Mapper接口:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;// 只需要繼承BaseMapper就能省去所有的單表CRUD
public interface UserMapper extends BaseMapper<User> {
}

MyBatisPlus的CRUD測(cè)試類(lèi):

@SpringBootTest
class MyBatisPlusUserMapperTests {@Autowiredprivate UserMapper userMapper;@Testvoid testInsert() {User user = new User();user.setId(5L);user.setUsername("Lucy");user.setPassword("123");user.setPhone("18688990011");user.setBalance(200);user.setInfo("{\"age\": 24, \"intro\": \"英文老師\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());userMapper.insert(user);}@Testvoid testSelectById() {User user = userMapper.selectById(5L);System.out.println("user = " + user);}@Testvoid testSelectByIds() {List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L));users.forEach(System.out::println);}@Testvoid testUpdateById() {User user = new User();user.setId(5L);user.setBalance(20000);userMapper.updateById(user);}@Testvoid testDelete() {userMapper.deleteById(5L);}
}

可以看到在運(yùn)行過(guò)程中打印出的SQL日志,以字段名進(jìn)行查詢(xún)而不是用*,非常標(biāo)準(zhǔn)。

11:52:03 DEBUG 22712 --- [           main] com.itheima.mp.mapper.UserMapper.insert  : ==>  Preparing: INSERT INTO user ( id, username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
11:52:03 DEBUG 22712 --- [           main] com.itheima.mp.mapper.UserMapper.insert  : ==> Parameters: 5(Long), Lucy(String), 123(String), 18688990011(String), {"age": 24, "intro": "英文老師", "gender": "female"}(String), 200(Integer), 2024-11-13T11:52:02.904878200(LocalDateTime), 2024-11-13T11:52:02.904878200(LocalDateTime)
11:52:03 DEBUG 22712 --- [           main] com.itheima.mp.mapper.UserMapper.insert  : <==    Updates: 111:54:23 DEBUG 8964 --- [           main] c.i.mp.mapper.UserMapper.selectBatchIds  : ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM user WHERE id IN ( ? , ? , ? , ? , ? )
11:54:23 DEBUG 8964 --- [           main] c.i.mp.mapper.UserMapper.selectBatchIds  : ==> Parameters: 1(Long), 2(Long), 3(Long), 4(Long), 5(Long)
11:54:23 DEBUG 8964 --- [           main] c.i.mp.mapper.UserMapper.selectBatchIds  : <==      Total: 5
User(id=1, username=Jack, password=123, phone=13900112224, info={"age": 20, "intro": "佛系青年", "gender": "male"}, status=1, balance=1600, createTime=2023-05-19T20:50:21, updateTime=2023-06-19T20:50:21)
User(id=2, username=Rose, password=123, phone=13900112223, info={"age": 19, "intro": "青澀少女", "gender": "female"}, status=1, balance=600, createTime=2023-05-19T21:00:23, updateTime=2023-06-19T21:00:23)
User(id=3, username=Hope, password=123, phone=13900112222, info={"age": 25, "intro": "上進(jìn)青年", "gender": "male"}, status=1, balance=100000, createTime=2023-06-19T22:37:44, updateTime=2023-06-19T22:37:44)
User(id=4, username=Thomas, password=123, phone=17701265258, info={"age": 29, "intro": "伏地魔", "gender": "male"}, status=1, balance=800, createTime=2023-06-19T23:44:45, updateTime=2023-06-19T23:44:45)
User(id=5, username=Lucy, password=123, phone=18688990011, info={"age": 24, "intro": "英文老師", "gender": "female"}, status=1, balance=200, createTime=2024-11-13T11:52:03, updateTime=2024-11-13T11:52:03)

3、常見(jiàn)注解

在剛剛的入門(mén)案例中,我們僅僅引入了依賴(lài),繼承了BaseMapper就能使用MybatisPlus,非常簡(jiǎn)單。但是問(wèn)題來(lái)了: MybatisPlus如何知道我們要查詢(xún)的是哪張表?表中有哪些字段呢?

UserMapper在繼承BaseMapper的時(shí)候指定了一個(gè)泛型

泛型中的User就是與數(shù)據(jù)庫(kù)對(duì)應(yīng)的PO實(shí)體類(lèi)。

MybatisPlus底層通過(guò)反射,根據(jù)PO實(shí)體的信息來(lái)推斷出表的信息,從而生成SQL的。默認(rèn)情況下(約定):

  • MybatisPlus會(huì)把PO實(shí)體的類(lèi)名駝峰轉(zhuǎn)下劃線作為表名
  • MybatisPlus會(huì)把PO實(shí)體的所有變量名駝峰轉(zhuǎn)下劃線作為表的字段名,并根據(jù)變量類(lèi)型推斷字段類(lèi)型
  • MybatisPlus會(huì)把名為id的字段作為主鍵

但很多情況下,默認(rèn)的實(shí)現(xiàn)與實(shí)際場(chǎng)景不符(實(shí)際情況與MP的約定不符合時(shí)使用),因此MybatisPlus提供了一些注解便于我們聲明表信息。

  • @TableName:用來(lái)指定表名
  • @Tableld:用來(lái)指定表中的主鍵字段信息
  • @TableField:用來(lái)指定表中的普通字段信息


(1)@TableName

  • 描述:表名注解,標(biāo)識(shí)實(shí)體類(lèi)對(duì)應(yīng)的表
  • 使用位置:實(shí)體類(lèi)

TableName注解除了指定表名以外,還可以指定很多其它屬性:

屬性類(lèi)型必須指定默認(rèn)值描述
valueString“”表名
schemaString“”schema
keepGlobalPrefixbooleanfalse是否保持使用全局的 tablePrefix 的值(當(dāng)全局 tablePrefix 生效時(shí))
resultMapString“”xml 中 resultMap 的 id(用于滿(mǎn)足特定類(lèi)型的實(shí)體類(lèi)對(duì)象綁定)
autoResultMapbooleanfalse是否自動(dòng)構(gòu)建 resultMap 并使用(如果設(shè)置 resultMap 則不會(huì)進(jìn)行 resultMap 的自動(dòng)構(gòu)建與注入)
excludePropertyString[]{}需要排除的屬性名 @since 3.3.1

(2)@TableId

  • 描述:主鍵注解,標(biāo)識(shí)實(shí)體類(lèi)中的主鍵字段
  • 使用位置:實(shí)體類(lèi)的主鍵字段

TableId注解支持兩個(gè)屬性:

屬性類(lèi)型必須指定默認(rèn)值描述
valueString“”表名
typeEnumIdType.NONE指定主鍵類(lèi)型

枚舉IdType支持的類(lèi)型有:

描述
AUTO數(shù)據(jù)庫(kù) ID 自增
NONE無(wú)狀態(tài),該類(lèi)型為未設(shè)置主鍵類(lèi)型(如果全局配置中有 IdType 相關(guān)的配置,則會(huì)跟隨全局配置。)當(dāng)我們?cè)O(shè)置 @TableId 類(lèi)型為NONE 時(shí),且不手動(dòng)設(shè)置主鍵值,MyBatisPlus將默認(rèn)給出一個(gè) Long 類(lèi)型的字符串,因?yàn)槿峙渲媚J(rèn)為ASSIGN_ID。
INPUTinsert 前自行 set 主鍵值。當(dāng)我們沒(méi)有設(shè)置主鍵值時(shí),MyBatisPlus并不設(shè)置 Long 類(lèi)型的值,而是插入為null。
ASSIGN_ID分配 ID(主鍵類(lèi)型為 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默認(rèn)實(shí)現(xiàn)類(lèi)為DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID分配 UUID,主鍵類(lèi)型為 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默認(rèn) default 方法)
ID_WORKER分布式全局唯一 ID 長(zhǎng)整型類(lèi)型 (please use ASSIGN_ID)
UUID32 位 UUID 字符串 (please use ASSIGN_UUID)
ID_WORKER_STR分布式全局唯一 ID 字符串類(lèi)型 (please use ASSIGN_ID)

這里比較常見(jiàn)的有三種:

  • AUTO:利用數(shù)據(jù)庫(kù)的id自增長(zhǎng)
  • INPUT:手動(dòng)生成id
  • ASSIGN_ID:雪花算法生成Long類(lèi)型的全局唯一id,這是默認(rèn)的ID策略

(3)@TableField

  • 描述:普通字段注解

一般情況下我們并不需要給字段添加@TableField注解,一些特殊情況除外:

  • 成員變量名與數(shù)據(jù)庫(kù)字段名不一致
  • 成員變量是以isXXX命名,按照JavaBean的規(guī)范,MybatisPlus識(shí)別字段時(shí)會(huì)把is去除,這就導(dǎo)致與數(shù)據(jù)庫(kù)不符。
  • 成員變量名與數(shù)據(jù)庫(kù)一致,但是與數(shù)據(jù)庫(kù)的關(guān)鍵字沖突。使用@TableField注解給字段名添加轉(zhuǎn)義字符(兩個(gè)反引號(hào)):````

支持的其它屬性如下:

屬性類(lèi)型必填默認(rèn)值描述
valueString“”數(shù)據(jù)庫(kù)字段名
existbooleantrue是否為數(shù)據(jù)庫(kù)表字段
conditionString“”字段 where 實(shí)體查詢(xún)比較條件,有值設(shè)置則按設(shè)置的值為準(zhǔn),沒(méi)有則為默認(rèn)全局的 %s=#{%s},參考(opens new window)
updateString“”字段 update set 部分注入,例如:當(dāng)在version字段上注解update=“%s+1” 表示更新時(shí)會(huì) set version=version+1 (該屬性?xún)?yōu)先級(jí)高于 el 屬性)
insertStrategyEnumFieldStrategy.DEFAULT舉例:NOT_NULL insert into table_a(column) values (#{columnProperty})
updateStrategyEnumFieldStrategy.DEFAULT舉例:IGNORED update table_a set column=#{columnProperty}
whereStrategyEnumFieldStrategy.DEFAULT舉例:NOT_EMPTY where column=#{columnProperty}
fillEnumFieldFill.DEFAULT字段自動(dòng)填充策略
selectbooleantrue是否進(jìn)行 select 查詢(xún)
keepGlobalFormatbooleanfalse是否保持使用全局的 format 進(jìn)行處理
jdbcTypeJdbcTypeJdbcType.UNDEFINEDJDBC 類(lèi)型 (該默認(rèn)值不代表會(huì)按照該值生效)
typeHandlerTypeHander類(lèi)型處理器 (該默認(rèn)值不代表會(huì)按照該值生效)
numericScaleString“”指定小數(shù)點(diǎn)后保留的位數(shù)

(4)使用案例

tb_user為MP這三個(gè)常用注解的演示表

  • MpUser為PO實(shí)體類(lèi)
@Data
@TableName("tb_user")
public class MpUser {// 用戶(hù)id@TableId(value = "user_id", type = IdType.AUTO)private Long id;// 用戶(hù)名@TableField("username")private String name;// 密碼private String password;// 是否被邏輯刪除@TableField("is_deleted")private Boolean isDeleted;// 排序字段@TableField("`order`")private Integer order;// 地址@TableField(exist = false)private String address;
}
  • MpUserMapper
public interface MpUserMapper extends BaseMapper<MpUser> {
}
  • MpUserMapperTests測(cè)試類(lèi)
@SpringBootTest
public class MpUserMapperTests {@Autowiredprivate MpUserMapper mpUserMapper;@Testvoid testInsert() {MpUser mpUser = new MpUser();//mpUser.setId(1L);mpUser.setName("MP用戶(hù)1");mpUser.setPassword("123");mpUser.setIsDeleted(false);mpUser.setOrder(1);mpUser.setAddress("https://baomidou.com/");mpUserMapper.insert(mpUser);}@Testvoid testSelectById() {MpUser mpUser = mpUserMapper.selectById(1L);System.out.println("mpUser = " + mpUser);}@Testvoid testSelectAll() {// selectList()方法的參數(shù)為MP內(nèi)置的條件封裝器Wrapper,所以不填寫(xiě)就是無(wú)任何條件,即查詢(xún)?nèi)?/span>List<MpUser> mpUserList = mpUserMapper.selectList(null);mpUserList.forEach(System.out::println);}@Testvoid testUpdateById() {MpUser mpUser = new MpUser();mpUser.setId(1L);mpUser.setIsDeleted(true);mpUserMapper.updateById(mpUser);}@Testvoid testDelete() {mpUserMapper.deleteById(1L);}
}

在新增時(shí)無(wú)論是否手動(dòng)設(shè)置id,主鍵字段都被忽略,由數(shù)據(jù)庫(kù)自增長(zhǎng)

在查詢(xún)時(shí),字段名會(huì)添加別名,沖突字段加了轉(zhuǎn)移符,并且查數(shù)據(jù)庫(kù)不存在的字段也不會(huì)報(bào)錯(cuò),而是null


4、常見(jiàn)配置

MybatisPlus也支持基于yaml文件的自定義配置,詳見(jiàn)官方文檔使用配置。

mybatis-plus:type-aliases-package: com.itheima.mp.domain.po  # 別名包掃描,這項(xiàng)無(wú)默認(rèn)值,需要自己指定mapper-locations: "classpath*:/mapper/**/*.xml" # mapper.xml映射文件地址,默認(rèn)值configuration:map-underscore-to-camel-case: true  # 是否開(kāi)啟下劃線和駝峰命名的映射,默認(rèn)開(kāi)啟cache-enabled: true # mybatis二級(jí)緩存,默認(rèn)開(kāi)啟global-config:db-config:id-type: assign_id  # 默認(rèn)全局id生成策略為雪花算法update-strategy: not_null # 默認(rèn)更新策略:只更新非null字段

mapper-locations:指定 MyBatis Mapper 對(duì)應(yīng)的 XML 文件位置。如果在 Mapper 中有自定義方法(手寫(xiě)SQL或多表聯(lián)查),需要配置此項(xiàng)。默認(rèn)值為:"classpath*:/mapper/**/*.xml",也就是說(shuō)我們只要把mapper.xml文件放置這個(gè)目錄下就一定會(huì)被加載。

對(duì)于 Maven 多模塊項(xiàng)目,掃描路徑應(yīng)以 classpath*: 開(kāi)頭,以加載多個(gè) JAR 包中的 XML 文件。

大多數(shù)的配置都有默認(rèn)值,因此我們都無(wú)需配置。但還有一些是沒(méi)有默認(rèn)值的,例如實(shí)體類(lèi)的別名掃描包type-aliases-package(默認(rèn)值為null),用于給包中的類(lèi)注冊(cè)別名,注冊(cè)后,在Mapper對(duì)應(yīng)的XML文件中可以直接使用類(lèi)名,無(wú)需使用全限定類(lèi)名。

另外如果數(shù)據(jù)庫(kù)的表大多數(shù)為主鍵自增,可以在全局配置中設(shè)置id-typeauto,之后如果有實(shí)體類(lèi)的id屬性是其他主鍵生成策略,再通過(guò)@TableId注解配置即可(優(yōu)先級(jí):指定注解 > 全局配置)。

mybatis-plus:type-aliases-package: com.itheima.mp.domain.poglobal-config:db-config:id-type: auto # 全局id類(lèi)型為自增長(zhǎng)



三、核心功能

剛才的案例中都是以id為條件的簡(jiǎn)單CRUD,一些復(fù)雜條件的SQL語(yǔ)句就要用到一些更高級(jí)的功能了。

1、條件構(gòu)造器

除了新增以外,修改、刪除、查詢(xún)的SQL語(yǔ)句都需要指定where條件。因此BaseMapper中提供的相關(guān)方法除了以id作為where條件以外,還支持更加復(fù)雜的where條件。

參數(shù)中的Wrapper就是條件構(gòu)造的抽象類(lèi),其下有很多默認(rèn)實(shí)現(xiàn),繼承關(guān)系如圖:

Wrapper的子類(lèi)AbstractWrapper提供了where中包含的所有條件構(gòu)造方法:

而QueryWrapper在AbstractWrapper的基礎(chǔ)上拓展了一個(gè)select方法,允許指定查詢(xún)字段:

而UpdateWrapper在AbstractWrapper的基礎(chǔ)上拓展了一個(gè)set方法,允許指定SQL中的SET部分:

接下來(lái),我們就來(lái)看看如何利用Wrapper實(shí)現(xiàn)復(fù)雜查詢(xún)。

(1)QueryWrapper

無(wú)論是修改、刪除、查詢(xún),都可以使用QueryWrapper來(lái)構(gòu)建查詢(xún)條件。接下來(lái)看一些例子:

查詢(xún):查詢(xún)出名字中帶o的,存款大于等于1000元的人的id、username、info、balance字段。

// 查詢(xún)出名字中帶o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testQueryWrapper() {// 1.構(gòu)建查詢(xún)條件 where username like "%o%" AND balance >= 1000QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2.查詢(xún)數(shù)據(jù)List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}

更新:更新用戶(hù)名為jack的用戶(hù)的余額為2000。

// 更新用戶(hù)名為jack的用戶(hù)的余額為2000。
@Test
void testUpdateByQueryWrapper() {// 1.設(shè)置要更新的數(shù)據(jù)User user = new User();user.setBalance(2000);// 2.構(gòu)建更新條件 where username = "Jack"QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("username", "Jack");// 3.執(zhí)行更新,user中非null字段都會(huì)作為set語(yǔ)句System.out.println(userMapper.update(user, queryWrapper) > 0);
}

(2)UpdateWrapper

基于BaseMapper中的update方法更新時(shí)只能直接賦值,對(duì)于一些復(fù)雜的需求就難以實(shí)現(xiàn)。 例如:更新id為1,2,4的用戶(hù)的余額,扣200,對(duì)應(yīng)的SQL應(yīng)該是:

UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET的賦值結(jié)果是基于字段現(xiàn)有值的,這個(gè)時(shí)候就要利用UpdateWrapper中的setSql功能了:

@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來(lái)更新System.out.println(userMapper.update(updateWrapper) > 0);
}

(3)基于Lambda的Wrapper

無(wú)論是QueryWrapper還是UpdateWrapper在構(gòu)造條件的時(shí)候都需要寫(xiě)死字段名稱(chēng),會(huì)出現(xiàn)字符串魔法值。這在編程規(guī)范中顯然是不推薦的。那怎么樣才能不寫(xiě)字段名,又能知道字段名呢?

其中一種辦法是基于變量的getter方法結(jié)合反射技術(shù),我們只要將條件對(duì)應(yīng)的字段的getter方法傳遞給MybatisPlus,它就能計(jì)算出對(duì)應(yīng)的變量名了。而傳遞方法可以使用JDK8中的方法引用和Lambda表達(dá)式。
因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含兩個(gè):

  • LambdaQueryWrapper,對(duì)應(yīng)QueryWrapper
  • LambdaUpdateWrapper,對(duì)應(yīng)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來(lái)更新System.out.println(userMapper.update(lambdaUpdateWrapper) > 0);
}@Test
void testLambdaQueryWrapper() {// 1.構(gòu)建查詢(xún)條件 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.查詢(xún)數(shù)據(jù)List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}

總結(jié):

  • QueryWrapper和LambdaQueryWrapper通常用來(lái)構(gòu)建select、delete、update的where條件部分
  • UpdateWrapper和LambdaUpdateWrapper通常只有在set語(yǔ)句比較特殊的情況才使用
  • 盡量使用LambdaQueryWrapper和LambdaUpdateWrapper避免硬編碼

2、自定義SQL

(1)自定義SQL片段

  • 問(wèn)題引出

在演示的UpdateWrapper和LambdaUpdateWrapper的案例中,我們?cè)诖a中編寫(xiě)了更新的SQL語(yǔ)句:

其中balance = balance - 200現(xiàn)在這種寫(xiě)法相當(dāng)于把Mapper層的sql語(yǔ)句寫(xiě)在Service層了,這在某些企業(yè)也是不允許的,因?yàn)镾QL語(yǔ)句最好都維護(hù)在持久層,而不是業(yè)務(wù)層。就當(dāng)前案例來(lái)說(shuō),由于條件是in語(yǔ)句,只能將SQL寫(xiě)在Mapper.xml文件,利用foreach來(lái)生成動(dòng)態(tài)SQL。 假如查詢(xún)條件更復(fù)雜,動(dòng)態(tài)SQL的編寫(xiě)也會(huì)更加復(fù)雜。

所以,MybatisPlus提供了自定義SQL片段功能,可以讓我們利用Wrapper生成查詢(xún)條件,再結(jié)合Mapper.xml編寫(xiě)SQL。

以當(dāng)前案例來(lái)說(shuō),我們可以這樣寫(xiě):

@Test
void testCustomSQLUpdate() {// 更新條件List<Long> ids = List.of(1L, 2L, 4L);int amount = 200;// 定義條件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 調(diào)用自定義SQL方法userMapper.updateBalanceByIds(wrapper, amount);
}

然后在UserMapper中自定義SQL:

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;public interface UserMapper extends BaseMapper<User> {@Select("UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}")//void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);void updateBalanceByIds(@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
}
  • ${ew.customSqlSegment}:為自定義SQL片段,@Param("ew")其中參數(shù)ew必須叫這個(gè),如果忘記了也可以用baomidou包下的常量類(lèi)Constants.WRAPPER,其值等于"ew"

這樣就省去了編寫(xiě)復(fù)雜查詢(xún)條件的煩惱了,總結(jié)一下自定義SQL片段的使用場(chǎng)景:

  1. 更新時(shí)的特殊場(chǎng)景,不是更新具體不變的值,而是在原有值的基礎(chǔ)上動(dòng)態(tài)做增減(例如balance = balance - amount),完全使用MP只能在業(yè)務(wù)層拼接這條SQL語(yǔ)句。此時(shí)可以使用自定義SQL傳值更新,更新的SQL定義在Mapper接口或Mapper.xml中,MP則更擅長(zhǎng)處理where更新條件。
  2. 查詢(xún)時(shí)的特殊場(chǎng)景(如下圖),查詢(xún)的字段結(jié)果是個(gè)別字段,而MP默認(rèn)查詢(xún)所有字段,只能通過(guò)QueryWrapper或LambdaQueryWrapper的select()方法去在業(yè)務(wù)層拼出查詢(xún)字段,有時(shí)為了不違背企業(yè)開(kāi)發(fā)規(guī)范,此時(shí)也可以使用自定義SQL片段


(2)多表聯(lián)查

理論上來(lái)講MyBatisPlus是不支持多表查詢(xún)的,不過(guò)我們可以利用Wrapper中自定義條件結(jié)合自定義SQL來(lái)實(shí)現(xiàn)多表查詢(xún)的效果。 例如,我們要查詢(xún)出所有收貨地址在北京的并且用戶(hù)id在1、2、4之中的用戶(hù)信息,要是自己基于mybatis實(shí)現(xiàn)SQL,大概是這樣的:

<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user uINNER JOIN address a ON u.id = a.user_idWHERE u.id<foreach collection="ids" separator="," item="id" open="IN (" close=")">#{id}</foreach>AND a.city = #{city}
</select>

可以看出其中最復(fù)雜的就是WHERE條件的編寫(xiě),如果業(yè)務(wù)復(fù)雜一些,這里的SQL會(huì)更變態(tài)。

但是基于自定義SQL結(jié)合Wrapper的玩法,我們就可以利用Wrapper來(lái)構(gòu)建查詢(xún)條件,然后手寫(xiě)SELECT及FROM部分,實(shí)現(xiàn)多表查詢(xún)。

查詢(xún)條件這樣來(lái)構(gòu)建:

@Test
void testCustomJoinWrapper() {// 1.準(zhǔn)備自定義查詢(xún)條件QueryWrapper<User> wrapper = new QueryWrapper<User>().in("u.id", List.of(1L, 2L, 4L)).eq("a.city", "北京");// 2.調(diào)用mapper的自定義方法List<User> users = userMapper.queryUserByWrapper(wrapper);users.forEach(System.out::println);
}

然后在UserMapper中自定義方法:

@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);

當(dāng)然,也可以在UserMapper.xml中寫(xiě)SQL:

<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>

3、IService接口

MybatisPlus不僅提供了BaseMapper,還提供了通用的Service接口及默認(rèn)實(shí)現(xiàn),封裝了一些常用的service模板方法。


通用接口為IService,默認(rèn)實(shí)現(xiàn)為ServiceImpl,其中封裝的方法可以分為以下幾類(lèi):

  • save:新增
  • remove:刪除
  • update:更新
  • get:查詢(xún)單個(gè)結(jié)果
  • list:查詢(xún)集合結(jié)果
  • count:計(jì)數(shù)
  • page:分頁(yè)查詢(xún)

(1)常用方法介紹

新增

  • save:新增單個(gè)元素
  • saveBatch:批量新增
  • saveOrUpdate:根據(jù)id判斷,如果實(shí)體類(lèi)中存在id就更新,不存在則新增
  • saveOrUpdateBatch:批量的新增或修改

刪除

  • removeById:根據(jù)id刪除
  • removeByIds:根據(jù)ids集合批量刪除
  • removeByMap:根據(jù)Map中的鍵值對(duì)為條件刪除
  • remove(Wrapper<T>):根據(jù)Wrapper條件刪除
  • removeBatchByIds:暫不支持

修改

  • updateById:根據(jù)id修改,只更新不為null的值
  • update(Wrapper<T>):根據(jù)UpdateWrapper修改,Wrapper中包含set和where部分
  • update(T,Wrapper):按照T內(nèi)的數(shù)據(jù)修改與Wrapper匹配到的數(shù)據(jù)
  • updateBatchById:根據(jù)id批量修改

查一條

  • getById:根據(jù)id查詢(xún)1條數(shù)據(jù)
  • getOne(Wrapper<T>):根據(jù)Wrapper查詢(xún)1條數(shù)據(jù)
  • getBaseMapper:獲取Service內(nèi)的BaseMapper實(shí)現(xiàn),某些時(shí)候需要直接調(diào)用Mapper內(nèi)的自定義SQL時(shí)可以用這個(gè)方法獲取到Mapper

查多條

  • listByIds:根據(jù)id批量查詢(xún)
  • list(Wrapper<T>):根據(jù)Wrapper條件查詢(xún)多條數(shù)據(jù)
  • list():查詢(xún)所有

查條數(shù)

  • count():統(tǒng)計(jì)所有數(shù)量
  • count(Wrapper<T>):統(tǒng)計(jì)符合Wrapper條件的數(shù)據(jù)數(shù)量

(2)基本用法

由于Service中經(jīng)常需要定義與業(yè)務(wù)有關(guān)的自定義方法,因此我們不能直接使用IService,而是自定義Service接口,然后繼承MP的IService接口以拓展方法。讓自定義的ServiceImpl實(shí)現(xiàn)類(lèi)實(shí)現(xiàn)自定義的Service接口,同時(shí)繼承MP的默認(rèn)實(shí)現(xiàn)類(lèi) ServiceImpl,同時(shí),這樣就不用自己實(shí)現(xiàn)IService接口中的方法了。

  1. 自定義Service接口繼承IService接口。定義IUserService繼承IService
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;public interface IUserService extends IService<User> {
}
  1. 自定義Service實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)自定義接口并繼承ServiceImpl類(lèi)。創(chuàng)建UserServiceImpl類(lèi),繼承ServiceImpl,實(shí)現(xiàn)UserService
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>接口的泛型參數(shù)中,M是繼承了BaseMapper的Mapper接口,T是PO實(shí)體類(lèi)。


(3)Restful案例

案例:基于Restful風(fēng)格實(shí)現(xiàn)下面的接口

編號(hào)接口請(qǐng)求方式請(qǐng)求路徑請(qǐng)求參數(shù)返回值
1新增用戶(hù)POST/users用戶(hù)表單實(shí)體無(wú)
2刪除用戶(hù)DELETE/users/{id}用戶(hù)id無(wú)
3根據(jù)id查詢(xún)用戶(hù)GET/users/{id}用戶(hù)id用戶(hù)VO
4根據(jù)id批量查詢(xún)GET/users用戶(hù)id集合用戶(hù)VO集合
5根據(jù)id扣減余額PUT/users/{id}/deduction/{money}+ 用戶(hù)id
+ 扣減金額
無(wú)

首先,我們?cè)陧?xiàng)目中引入Swagger和Web依賴(lài):

<!-- swagger -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.1.0</version>
</dependency>
<!-- web -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

在yaml中配置swagger信息:

knife4j:enable: trueopenapi:title: 用戶(hù)管理接口文檔description: "用戶(hù)管理接口文檔"email: Aizen@qq.comconcat: 藍(lán)染url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.itheima.mp.controller

然后,接口的接收和返回值分別需要定義兩個(gè)實(shí)體:

  • UserFormDTO:代表新增時(shí)的用戶(hù)表單
package com.itheima.mp.domain.dto;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用戶(hù)表單實(shí)體")
public class UserFormDTO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用戶(hù)名")private String username;@ApiModelProperty("密碼")private String password;@ApiModelProperty("注冊(cè)手機(jī)號(hào)")private String phone;@ApiModelProperty("詳細(xì)信息,JSON風(fēng)格")private String info;@ApiModelProperty("賬戶(hù)余額")private Integer balance;
}
  • UserVO:代表查詢(xún)的返回結(jié)果
package com.itheima.mp.domain.vo;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用戶(hù)VO實(shí)體")
public class UserVO {@ApiModelProperty("用戶(hù)id")private Long id;@ApiModelProperty("用戶(hù)名")private String username;@ApiModelProperty("詳細(xì)信息")private String info;@ApiModelProperty("使用狀態(tài)(1正常 2凍結(jié))")private Integer status;@ApiModelProperty("賬戶(hù)余額")private Integer balance;
}

最后,按照Restful風(fēng)格編寫(xiě)Controller接口方法

@Api(tags = "用戶(hù)管理接口")
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor    // lombok注解:在構(gòu)造方法中只會(huì)注入必須需要初始化的成員變量,例如加了final且未初始化的變量,將來(lái)不需要做注入的變量不加final即可
public class UserController {// Spring不推薦我們使用@Autowired進(jìn)行屬性注入,推薦我們使用構(gòu)造器注入,但當(dāng)需要注入的成員變量很多的時(shí)候,構(gòu)造方法會(huì)顯得特別長(zhǎng),因此我們可以將需要注入的變量加上final,并且使用lombok的@RequiredArgsConstructor注解提供必要參數(shù)構(gòu)造器private final IUserService userService;@ApiOperation("新增用戶(hù)接口")@PostMappingpublic void saveUser(@RequestBody UserFormDTO userDTO) {// 將DTO拷貝到POUser user = BeanUtil.copyProperties(userDTO, User.class);// 新增userService.save(user);}@ApiOperation("刪除用戶(hù)接口")@DeleteMapping("/{id}")public void deleteUserById(@ApiParam("用戶(hù)id") @PathVariable("id") Long id) {userService.removeById(id);}@ApiOperation("根據(jù)id查詢(xún)用戶(hù)接口")@GetMapping("/{id}")public UserVO queryUserById(@ApiParam("用戶(hù)id") @PathVariable("id") Long id) {// 查詢(xún)用戶(hù)POUser user = userService.getById(id);// 將PO拷貝到VOreturn BeanUtil.copyProperties(user, UserVO.class);}@ApiOperation("根據(jù)id批量查詢(xún)用戶(hù)接口")@GetMappingpublic List<UserVO> queryUserById(@ApiParam("用戶(hù)id集合") @RequestParam("ids") List<Long> ids) {// 查詢(xún)用戶(hù)PO集合List<User> users = userService.listByIds(ids);// 將PO集合拷貝到VO集合return BeanUtil.copyToList(users, UserVO.class);}@ApiOperation("根據(jù)id扣減用戶(hù)余額接口")@PutMapping("/{id}/deduction/{money}")public void deductBalanceById(@ApiParam("用戶(hù)id") @PathVariable("id") Long id,@ApiParam("扣減的金額") @PathVariable("money") Integer money) {userService.deductBalanceById(id, money);}
}

可以看到前四個(gè)接口都直接在Controller實(shí)現(xiàn)即可,無(wú)需編寫(xiě)任何Service代碼,非常方便。不過(guò),一些帶有業(yè)務(wù)邏輯的接口,比如第五個(gè)deductBalanceById接口,MP的Service沒(méi)有提供業(yè)務(wù)邏輯,所以這些業(yè)務(wù)邏輯都要在Service層來(lái)做。另外更新余額需要自定義SQL,要在mapper中來(lái)實(shí)現(xiàn)。

  • UserService接口和UserServiceImpl實(shí)現(xiàn)類(lèi)
public interface IUserService extends IService<User> {void deductBalanceById(Long id, Integer money);
}@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic void deductBalanceById(Long id, Integer money) {// 查詢(xún)用戶(hù)User user = getById(id);// 校驗(yàn)用戶(hù)狀態(tài)if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶(hù)狀態(tài)異常!");}// 校驗(yàn)余額是否充足if (user.getBalance() < money) {throw new RuntimeException("用戶(hù)余額不足!");}// 扣減余額baseMapper.deductBalance(id, money);}
}
  • mapper接口
public interface UserMapper extends BaseMapper<User> {@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
  • 訪問(wèn)http://localhost:8080/doc.html,測(cè)試接口


(4)LambdaQuery和LambdaUpdate

IService中還提供了Lambda功能來(lái)簡(jiǎn)化我們的復(fù)雜查詢(xún)及更新功能。我們通過(guò)兩個(gè)案例來(lái)學(xué)習(xí)一下。

案例一:實(shí)現(xiàn)一個(gè)根據(jù)復(fù)雜條件查詢(xún)用戶(hù)的接口,查詢(xún)條件如下:

  • name:用戶(hù)名關(guān)鍵字,可以為空
  • status:用戶(hù)狀態(tài),可以為空
  • minBalance:最小余額,可以為空
  • maxBalance:最大余額,可以為空

可以理解成一個(gè)用戶(hù)的后臺(tái)管理界面,管理員可以自己選擇條件來(lái)篩選用戶(hù),因此上述條件不一定存在,需要?jiǎng)討B(tài)做判斷。

我們首先需要定義一個(gè)查詢(xún)條件實(shí)體,UserQueryDTO實(shí)體:

@Data
@ApiModel(description = "用戶(hù)查詢(xún)條件實(shí)體")
public class UserQueryDTO {@ApiModelProperty("用戶(hù)名關(guān)鍵字")private String name;@ApiModelProperty("用戶(hù)狀態(tài):1-正常,2-凍結(jié)")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
  • 在UserController中定義方法
@ApiOperation("根據(jù)條件查詢(xún)用戶(hù)接口")
@GetMapping("/condition")
public List<UserVO> queryUserByCondition(UserQueryDTO queryDTO) {// 查詢(xún)用戶(hù)PO集合List<User> users = userService.queryUserByCondition(queryDTO);// 將PO集合拷貝到VO集合return BeanUtil.copyToList(users, UserVO.class);
}
  • UserService接口和UserServiceImpl實(shí)現(xiàn)類(lèi),基于lambdaQuery實(shí)現(xiàn)
public interface IUserService extends IService<User> {List<User> queryUserByCondition(UserQueryDTO queryDTO);
}// 基于Lambda查詢(xún)
@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對(duì)LambdaQueryWrapperLambdaUpdateWrapper的用法進(jìn)一步做了簡(jiǎn)化。我們無(wú)需自己通過(guò)new的方式來(lái)創(chuàng)建Wrapper,而是直接調(diào)用lambdaQuerylambdaUpdate方法。在組織查詢(xún)條件的時(shí)候,我們加入了name != null這樣的參數(shù),意思就是當(dāng)條件成立時(shí)才會(huì)添加這個(gè)查詢(xún)條件,類(lèi)似Mybatis的mapper.xml文件中的<if>標(biāo)簽。這樣就實(shí)現(xiàn)了動(dòng)態(tài)查詢(xún)條件效果了。

MybatisPlus會(huì)根據(jù)鏈?zhǔn)骄幊痰淖詈笠粋€(gè)方法來(lái)判斷最終的返回結(jié)果。lambdaQuery方法中除了可以構(gòu)建條件,還需要在鏈?zhǔn)骄幊痰淖詈筇砑右粋€(gè)查詢(xún)結(jié)果,list()表示查詢(xún)結(jié)果返回一個(gè)List集合??蛇x的常用方法有:

  • one():最多1個(gè)結(jié)果
  • list():返回集合結(jié)果
  • count():返回計(jì)數(shù)結(jié)果
  • exist():返回查詢(xún)的結(jié)果是否存在

與lambdaQuery方法類(lèi)似,IService中的lambdaUpdate方法可以非常方便的實(shí)現(xiàn)復(fù)雜更新業(yè)務(wù)。

案例二:改造根據(jù)id修改用戶(hù)余額的接口,要求如下

  • 完成對(duì)用戶(hù)狀態(tài)校驗(yàn)
  • 完成對(duì)用戶(hù)余額校驗(yàn)
  • 如果扣減后余額為0,則將用戶(hù)status修改為2,表示凍結(jié)狀態(tài)(update語(yǔ)句的set部分是動(dòng)態(tài)的)
  • 基于lambdaUpdate實(shí)現(xiàn)
@Override
@Transactional
public void deductBalanceById(Long id, Integer money) {// 查詢(xún)用戶(hù)User user = getById(id);// 校驗(yàn)用戶(hù)狀態(tài)if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶(hù)狀態(tài)異常!");}// 校驗(yàn)余額是否充足if (user.getBalance() < money) {throw new RuntimeException("用戶(hù)余額不足!");}// 扣減余額//baseMapper.deductBalance(id, money);int remainBalance = user.getBalance() - money;lambdaUpdate().set(User::getBalance, remainBalance)   // 更新余額.set(remainBalance == 0, User::getStatus, 2)    // 動(dòng)態(tài)判斷是否更新status.eq(User::getBalance, user.getBalance())    // CAS樂(lè)觀鎖.eq(User::getId, id)   // 根據(jù)id扣減對(duì)應(yīng)用戶(hù)的余額.update();  // 注意:LambdaUpdate做復(fù)雜更新時(shí),最后必須記得加上.update()進(jìn)行更新操作
}

(5)批量新增 & 批處理方案性能測(cè)試

IService中的批量新增功能使用起來(lái)非常方便,但有一點(diǎn)注意事項(xiàng)。

需求:批量插入10萬(wàn)條用戶(hù)數(shù)據(jù),并作出對(duì)比。

  • 方式一:普通for循環(huán)逐條插入
  • 方式二:IService的批量插入(默認(rèn)不開(kāi)啟 jdbc 批處理參數(shù))
  • 方式三:開(kāi)啟rewriteBatchedStatements=true參數(shù)

首先我們測(cè)試方式一,逐條插入數(shù)據(jù)

/*** 10w次插入意味著10w次網(wǎng)絡(luò)請(qǐng)求,耗時(shí)最慢*/
@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("耗時(shí):" + (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;
}

執(zhí)行結(jié)果耗時(shí)大約為551.9秒

可以看到速度非常慢。

再測(cè)試一下方式二,MybatisPlus的批處理:

/*** MP批處理采用的是JDBC底層的預(yù)編譯方案PreparedStatement,將1000條數(shù)據(jù)統(tǒng)一打包執(zhí)行save一并提交到MySQL,每1000條發(fā)送一次網(wǎng)絡(luò)請(qǐng)求,插入100次共發(fā)送100次網(wǎng)絡(luò)請(qǐng)求* MP如果不加JDBC連接參數(shù)rewriteBatchedStatements=true,底層還是打包逐條插入,只不過(guò)是從網(wǎng)絡(luò)請(qǐng)求數(shù)量上減少了耗時(shí)* 而加上了MySQL的這個(gè)開(kāi)啟批處理參數(shù)后,MP調(diào)用的JDBC底層的批處理才能真正變成一次性批量插入多條數(shù)據(jù)*/
@Test
void testSaveBatch() {// 因?yàn)橐淮涡詎ew 10萬(wàn)條數(shù)據(jù)占用內(nèi)存太多,并且向數(shù)據(jù)庫(kù)請(qǐng)求的數(shù)據(jù)包有上限大小限制(一次網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量是有限的)// 所以我們每次批量插入1000條件,插入100次即10萬(wàn)條數(shù)據(jù)// 準(zhǔn)備一個(gè)容量為1000的集合List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {// 添加一個(gè)userlist.add(buildUser(i));// 每1000條批量插入一次if (i % 1000 == 0) {// 批量插入userService.saveBatch(list);// 清空集合,準(zhǔn)備下一批數(shù)據(jù)list.clear();}}long e = System.currentTimeMillis();System.out.println("耗時(shí):" + (e - b));
}

執(zhí)行結(jié)果耗時(shí)大約為27.6秒,打包逐條插入,從網(wǎng)絡(luò)請(qǐng)求層面大大減少了耗時(shí)。

雖然上面的方式二減少了網(wǎng)絡(luò)請(qǐng)求次數(shù),但是底層還是打包逐條SQL插入。如果想要真正實(shí)現(xiàn)批量處理,有下面兩種辦法。

第一種實(shí)現(xiàn)就是利用MyBatis自定義SQL語(yǔ)句,用<foreach>標(biāo)簽遍歷組裝成下面一條SQL的形式

<!-- MyBatis批量插入 -->
<insert id="batchInsertUsers">INSERT INTO user (username, password, phone, info, balance, create_time, update_time)VALUES<foreach collection="list" item="user" separator=",">(#{name}, #{password}, #{phone}, #{info}, #{balance}, #{createTime}, #{updateTime})</foreach>
</insert>

第二種實(shí)現(xiàn),就是還是利用MP的jdbc批處理,只不過(guò)MySQL本身默認(rèn)沒(méi)有開(kāi)啟這個(gè)批處理參數(shù)rewriteBatchedStatements=true,該參數(shù)在MySQL 3.1.13版本開(kāi)始引入,默認(rèn)值為false不開(kāi)啟。因此這個(gè)批處理操作其實(shí)底層是由MySQL驅(qū)動(dòng)去做的,不是由MP來(lái)做的。所以我們只需在jdbc的url連接參數(shù)后添加該參數(shù),MP的批處理才能成效。

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

jdbc批處理相比于Mybatis批處理效率更高

  • 開(kāi)啟參數(shù)后,測(cè)試耗時(shí)大約為6.5秒

拓展rewriteBatchedStatements=trueallowMultiQueries=true 的區(qū)別

  • rewriteBatchedStatements是重寫(xiě)sql語(yǔ)句達(dá)到發(fā)送一次sql的請(qǐng)求效果;allowMultiQueries是在mapper.xml中使用;分號(hào)分隔多條sql,允許多條語(yǔ)句一起發(fā)送執(zhí)行的。
  • 對(duì)于insert批處理操作,開(kāi)啟rewriteBatchedStatements=true,驅(qū)動(dòng)則會(huì)把多條sql語(yǔ)句重寫(xiě)成一條sql語(yǔ)句然后再發(fā)出去;而對(duì)于update和delete批處理操作,開(kāi)啟allowMultiQueries=true,驅(qū)動(dòng)所做的事就是把多條sql語(yǔ)句累積起來(lái)再一次性發(fā)出去。

批處理方案總結(jié):

  • 普通for循環(huán)逐條插入速度極差,不推薦
  • MP的批量新增,基于預(yù)編譯的批處理,性能不錯(cuò)
  • 配置jdbc參數(shù),開(kāi)啟rewriteBatchedStatements,性能最好



四、擴(kuò)展功能

1、代碼生成

在使用MybatisPlus以后,基礎(chǔ)的Mapper、Service、PO代碼相對(duì)固定,重復(fù)編寫(xiě)也比較麻煩。因此MybatisPlus官方提供了代碼生成器根據(jù)數(shù)據(jù)庫(kù)表結(jié)構(gòu)生成PO、MapperService等相關(guān)代碼。只不過(guò)代碼生成器同樣要編碼使用,也很麻煩。

這里推薦大家使用一款MybatisPlus的插件,它可以基于圖形化界面完成MybatisPlus的代碼生成,非常簡(jiǎn)單。


(1)安裝插件

在idea的plugins市場(chǎng)中搜索并安裝MyBatisPlus插件:


(2)使用步驟

剛好數(shù)據(jù)庫(kù)中還有一張address表尚未生成對(duì)應(yīng)的實(shí)體和mapper等基礎(chǔ)代碼。我們利用插件生成一下。 首先需要配置數(shù)據(jù)庫(kù)地址,在Idea頂部菜單中,找到other,選擇Config Database,在彈出的窗口中填寫(xiě)數(shù)據(jù)庫(kù)連接的基本信息:

點(diǎn)擊OK保存。然后再次點(diǎn)擊Idea頂部菜單中的other,然后選擇Code Generator,在彈出的表單中填寫(xiě)信息:

最終,代碼自動(dòng)生成到指定的位置了


(3)代碼生成器配置

如果不想用圖形化界面方式配置生成代碼,使用MyBatis-Plus官網(wǎng)提供的代碼生成器模板也是可以的,但需要自己填寫(xiě)配置信息。

因?yàn)镸P代碼生成更新迭代速度很快,若本文的API被棄用,請(qǐng)以官網(wǎng)最新版本API為準(zhǔn):

MyBatis-Plus新代碼生成器:https://baomidou.com/guides/new-code-generator/

代碼生成器配置:https://baomidou.com/reference/new-code-generator-configuration/

  • 引入依賴(lài)
<!-- 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 {/*** 數(shù)據(jù)庫(kù)鏈接地址**/private static final String JDBC_URL_MAN = "jdbc:mysql://xxxxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8";/*** 數(shù)據(jù)庫(kù)登錄賬號(hào)**/private static final String JDBC_USER_NAME = "xx";/*** 數(shù)據(jù)庫(kù)登錄密碼**/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")    // 輸出路徑(寫(xiě)到j(luò)ava目錄).enableSwagger()                                                          // 開(kāi)啟swagger.commentDate("yyyy-MM-dd");                                       // 設(shè)置注釋日期格式,默認(rèn)值: yyyy-MM-dd}).packageConfig(builder -> {builder.parent("com.{company}")     // 設(shè)置父包名.moduleName("{model}")      // 設(shè)置父包模塊名.entity("domain")           // 設(shè)置實(shí)體類(lèi)包名.service("service")         // 設(shè)置Service接口包名.serviceImpl("service.impl")// 設(shè)置Service實(shí)現(xiàn)類(lèi)包名.controller("controller")   // 設(shè)置Controller包名.mapper("mapper")           // 設(shè)置Mapper接口文件包名.xml("mappers")             // 設(shè)置Mapper XML文件包名.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + dir + "\\src\\main\\resources\\mapper"));}).strategyConfig(builder -> {builder.addInclude(tables)                              // 設(shè)置需要生成的表名.addTablePrefix(tablePrefix)                    // 設(shè)置表前綴.serviceBuilder()                               // 設(shè)置 Service 層模板.formatServiceFileName("%sService").formatServiceImplFileName("%sServiceImpl").entityBuilder()                                // 設(shè)置實(shí)體類(lèi)模板.enableLombok()                                 // 啟用 Lombok.logicDeleteColumnName("deleted")               // 邏輯刪除字段名(數(shù)據(jù)庫(kù)字段).enableTableFieldAnnotation()                   // 開(kāi)啟生成實(shí)體時(shí)生成字段注解.controllerBuilder().formatFileName("%sController").enableRestStyle()                              // 啟用 REST 風(fēng)格.mapperBuilder()                                // Mapper 策略配置.enableBaseResultMap()                          // 生成通用的resultMap.superClass(BaseMapper.class)                   // 設(shè)置父類(lèi).formatMapperFileName("%sMapper").enableMapperAnnotation().formatXmlFileName("%sMapper");})//.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默認(rèn)的是Velocity引擎模板.execute();     // 執(zhí)行生成}
}

2、靜態(tài)工具

有的時(shí)候Service之間也會(huì)相互調(diào)用,為了避免出現(xiàn)循環(huán)依賴(lài)問(wèn)題,MybatisPlus提供一個(gè)靜態(tài)工具類(lèi):Db,其中的一些靜態(tài)方法與IService中方法簽名基本一致,也可以幫助我們實(shí)現(xiàn)CRUD功能:

因?yàn)殪o態(tài)方法無(wú)法讀取類(lèi)上的泛型,所以MP在使用靜態(tài)工具讀取表信息時(shí),需要傳入PO實(shí)體類(lèi)的Class字節(jié)碼,MP再通過(guò)反射獲取到表信息。其中新增和修改的方法由于需要傳入實(shí)體類(lèi)對(duì)象,因此不用傳入實(shí)體類(lèi)的Class字節(jié)碼。下面是使用示例:

@Test
void testDbGet() {User user = Db.getById(1L, User.class);System.out.println(user);
}@Test
void testDbList() {// 利用Db實(shí)現(xiàn)復(fù)雜條件查詢(xún)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");
}

(1)案例一

  • 案例一:改造根據(jù)id用戶(hù)查詢(xún)的接口,查詢(xún)用戶(hù)的同時(shí)返回用戶(hù)收貨地址列表

首先,我們要添加一個(gè)收貨地址的VO對(duì)象AddressVO

@Data
@ApiModel(description = "收貨地址VO")
public class AddressVO{@ApiModelProperty("id")private Long id;@ApiModelProperty("用戶(hù)ID")private Long userId;@ApiModelProperty("省")private String province;@ApiModelProperty("市")private String city;@ApiModelProperty("縣/區(qū)")private String town;@ApiModelProperty("手機(jī)")private String mobile;@ApiModelProperty("詳細(xì)地址")private String street;@ApiModelProperty("聯(lián)系人")private String contact;@ApiModelProperty("是否是默認(rèn) 1默認(rèn) 0否")private Boolean isDefault;@ApiModelProperty("備注")private String notes;
}

然后,改造原來(lái)的UserVO,添加一個(gè)用戶(hù)的收獲地址集合(對(duì)多)屬性

@Data
@ApiModel(description = "用戶(hù)VO實(shí)體")
public class UserVO {@ApiModelProperty("用戶(hù)id")private Long id;@ApiModelProperty("用戶(hù)名")private String username;@ApiModelProperty("詳細(xì)信息")private String info;@ApiModelProperty("使用狀態(tài)(1正常 2凍結(jié))")private Integer status;@ApiModelProperty("賬戶(hù)余額")private Integer balance;@ApiModelProperty("用戶(hù)的收獲地址")private List<AddressVO> addresses;
}

修改UserController中根據(jù)id查詢(xún)用戶(hù)的業(yè)務(wù)接口

@ApiOperation("根據(jù)id查詢(xún)用戶(hù)接口")
@GetMapping("/{id}")
public UserVO queryUserById(@ApiParam("用戶(hù)id") @PathVariable("id") Long id) {return userService.queryUserAndAddressById(id);
}

IUserService和UserServiceImpl

public interface IUserService extends IService<User> {UserVO queryUserAndAddressById(Long id);
}@Override
public UserVO queryUserAndAddressById(Long id) {// 查詢(xún)用戶(hù)POUser user = this.getById(id);if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶(hù)不存在或用戶(hù)狀態(tài)異常!");}// 查詢(xún)地址POList<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();// 封裝VO,將用戶(hù)PO轉(zhuǎn)為VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 如果地址PO不為空,將地址PO集合轉(zhuǎn)化為地址VO集合,設(shè)置到用戶(hù)VO中if (CollUtil.isNotEmpty(addresses)) {List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);userVO.setAddresses(addressVOList);}return userVO;
}

在查詢(xún)地址時(shí),我們采用了Db的靜態(tài)方法,因此避免了注入AddressService,減少了循環(huán)依賴(lài)的風(fēng)險(xiǎn)。


(2)案例二

  • 案例二:改造根據(jù)id批量查詢(xún)用戶(hù)的接口,查詢(xún)用戶(hù)的同時(shí),查詢(xún)出用戶(hù)對(duì)應(yīng)的所有地址

代碼實(shí)現(xiàn):

@ApiOperation("根據(jù)id批量查詢(xún)用戶(hù)接口")
@GetMapping
public List<UserVO> queryUserByIds(@ApiParam("用戶(hù)id集合") @RequestParam("ids") List<Long> ids) {return userService.queryUserAndAddressByIds(ids);
}public interface IUserService extends IService<User> {List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {// 查詢(xún)用戶(hù)List<User> users = this.listByIds(ids);if (CollUtil.isEmpty(users)) {return Collections.emptyList();}// 查詢(xún)地址// 獲取用戶(hù)id集合List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());// 根據(jù)用戶(hù)id集合查詢(xún)地址PO集合List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();// 地址PO集合轉(zhuǎn)地址VO集合List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);// 用戶(hù)地址集合分組處理,相同用戶(hù)的放入一個(gè)集合(組)中Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);if (CollUtil.isNotEmpty(addressVOList)) {addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));}// PO集合轉(zhuǎn)VO集合返回List<UserVO> userVOList = new ArrayList<>(users.size());for (User user : users) {// 轉(zhuǎn)換用戶(hù)PO為VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 為每個(gè)用戶(hù)VO設(shè)置自己的地址集合userVO.setAddresses(addressMap.get(user.getId()));userVOList.add(userVO);}return userVOList;
}

(3)案例三

  • 案例三:實(shí)現(xiàn)根據(jù)用戶(hù)id查詢(xún)收貨地址功能,需要驗(yàn)證用戶(hù)狀態(tài),凍結(jié)用戶(hù)拋出異常

代碼實(shí)現(xiàn):

@Api(tags = "地址管理接口")
@RestController
@RequestMapping("/address")
@RequiredArgsConstructor
public class AddressController {private final IAddressService addressService;@ApiOperation("根據(jù)用戶(hù)id查詢(xún)地址接口")@GetMapping("/{userId}")public List<AddressVO> queryAddressByUserId(@ApiParam("用戶(hù)id") @PathVariable("userId") Long userId) {return addressService.queryAddressByUserId(userId);}
}public interface IAddressService extends IService<Address> {List<AddressVO> queryAddressByUserId(Long userId);
}@Service
public class AddressServiceImpl extends ServiceImpl<AddressMapper, Address> implements IAddressService {@Overridepublic List<AddressVO> queryAddressByUserId(Long userId) {// 查詢(xún)用戶(hù),驗(yàn)證用戶(hù)狀態(tài),凍結(jié)用戶(hù)拋出異常User user = Db.getById(userId, User.class);if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶(hù)不存在或用戶(hù)狀態(tài)異常!");}// 查詢(xún)?cè)撚脩?hù)的收獲地址List<Address> addresses = lambdaQuery().eq(Address::getUserId, userId).list();return BeanUtil.copyToList(addresses, AddressVO.class);}
}

3、邏輯刪除

(1)介紹

對(duì)于一些比較重要的數(shù)據(jù),我們往往會(huì)采用邏輯刪除的方案,即:

  • 在表中添加一個(gè)字段標(biāo)記數(shù)據(jù)是否被刪除,邏輯刪除字段的屬性通常是IntegerBoolean類(lèi)型。
  • 當(dāng)刪除數(shù)據(jù)時(shí)把標(biāo)記置為1,1表示已刪除
  • 查詢(xún)時(shí)只查詢(xún)標(biāo)記為0的數(shù)據(jù),0表示未刪除

同理更新操作也需要加上deleted = 0,所以一旦采用了邏輯刪除,所有的查詢(xún)、刪除、更新邏輯都要跟著變化,非常麻煩。

為了解決這個(gè)問(wèn)題,MybatisPlus就添加了對(duì)邏輯刪除的支持。無(wú)需改變方法調(diào)用的方式,而是在底層幫我們自動(dòng)修改CRUD的語(yǔ)句。我們只需要在application.yaml文件中配置邏輯刪除的字段名稱(chēng)和值即可。

在這里插入圖片描述

注意:只有MybatisPlus生成的SQL語(yǔ)句才支持自動(dòng)的邏輯刪除,自定義SQL需要自己手動(dòng)處理邏輯刪除。


(2)使用步驟

例如,給address表添加一個(gè)邏輯刪除字段

alter table address add deleted bit default b'0' null comment '邏輯刪除';

Address實(shí)體類(lèi)添加deleted屬性

application.yaml中配置MP邏輯刪除字段

mybatis-plus:global-config:db-config:logic-delete-field: deleted # 全局邏輯刪除的實(shí)體字段名(since 3.3.0,配置后可以忽略不配置步驟2)logic-delete-value: 1 # 邏輯已刪除值(默認(rèn)為 1)logic-not-delete-value: 0 # 邏輯未刪除值(默認(rèn)為 0)
  • 測(cè)試刪除和查詢(xún)
@SpringBootTest
class IAddressServiceTest {@Autowiredprivate IAddressService addressService;@Testvoid testLogicDelete() {// 刪除addressService.removeById(59L);// 查詢(xún)Address address = addressService.getById(59L);System.out.println("address = " + address);}
}

對(duì)于沒(méi)有邏輯刪除字段的表不受影響,刪除和查詢(xún)還和之前一樣。


(3)@TableLogic

  • @TableLogic注解用于標(biāo)記實(shí)體類(lèi)中的邏輯刪除字段。
@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注解用于標(biāo)記實(shí)體類(lèi)中的邏輯刪除字段//@TableLogic(value = "0", delval = "1") // value表示默認(rèn)邏輯未刪除值,delval表示默認(rèn)邏輯刪除值 (這兩個(gè)值可無(wú)、會(huì)自動(dòng)獲取全局配置)@TableField("deleted")private Boolean deleted;
}

使用這種方式就不用在application.yaml中配置MP邏輯刪除字段了,直接在邏輯刪除字段屬性上加該注解即可。

總結(jié):邏輯刪除本身也有自己的問(wèn)題,比如

  • 會(huì)導(dǎo)致數(shù)據(jù)庫(kù)表垃圾數(shù)據(jù)越來(lái)越多,從而影響查詢(xún)效率
  • SQL中全都需要對(duì)邏輯刪除字段做判斷,影響查詢(xún)效率

因此,不太推薦采用邏輯刪除功能,如果數(shù)據(jù)不能刪除,可以采用把數(shù)據(jù)遷移到其它表的辦法。


4、枚舉處理器

當(dāng)實(shí)體類(lèi)屬性是枚舉類(lèi)型,在與數(shù)據(jù)庫(kù)的字段類(lèi)型做轉(zhuǎn)換時(shí),底層默認(rèn)使用的是MyBatis提供的EnumOrdinalTypeHandler枚舉類(lèi)型處理器。

但是這個(gè)并不好用,所以MP對(duì)類(lèi)型處理器做了增強(qiáng),其中增強(qiáng)后的枚舉處理器叫MybatisEnumTypeHandler,JSON處理器叫AbstractJsonTypeHandler


(1)定義枚舉,標(biāo)記@EnumValue

定義一個(gè)用戶(hù)狀態(tài)的枚舉

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "凍結(jié)");@EnumValueprivate final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}

要讓MybatisPlus處理枚舉與數(shù)據(jù)庫(kù)類(lèi)型自動(dòng)轉(zhuǎn)換,我們必須告訴MybatisPlus,枚舉中的哪個(gè)字段的值作為數(shù)據(jù)庫(kù)值。 MybatisPlus提供了@EnumValue注解來(lái)標(biāo)記枚舉屬性值。因此我們需要給枚舉中與數(shù)據(jù)庫(kù)字段類(lèi)型對(duì)應(yīng)的屬性值添加@EnumValue注解。

UserUserVO類(lèi)中的status字段類(lèi)型改為UserStatus 枚舉類(lèi)型。

(2)配置枚舉處理器

在application.yaml文件中配置枚舉處理器

mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  • 測(cè)試發(fā)現(xiàn)查詢(xún)出的status字段會(huì)是枚舉類(lèi)型,默認(rèn)顯示的是枚舉項(xiàng)的名稱(chēng)

如果我們想向前端返回指定的枚舉屬性,例如value狀態(tài)值desc描述,SpringMVC負(fù)責(zé)處理響應(yīng)數(shù)據(jù),它在底層處理Json時(shí)用的是jackson,所以我們只需要使用jackson提供的@JsonValue注解,來(lái)標(biāo)記JSON序列化后展示的字段。

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "凍結(jié)");@EnumValueprivate final int value;@JsonValueprivate final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
  • 響應(yīng)效果


5、JSON類(lèi)型處理器

數(shù)據(jù)庫(kù)的user表中有一個(gè)info字段,是JSON類(lèi)型

info的格式像這樣

{"age": 20, "intro": "佛系青年", "gender": "male"}

這樣一來(lái),我們要讀取info中的屬性時(shí)就非常不方便。如果要方便獲取,info的類(lèi)型最好是一個(gè)Map或者實(shí)體類(lèi)。而一旦我們把info改為對(duì)象類(lèi)型,就需要在寫(xiě)入數(shù)據(jù)庫(kù)時(shí)手動(dòng)轉(zhuǎn)為String,再讀取數(shù)據(jù)庫(kù)時(shí),手動(dòng)轉(zhuǎn)換為對(duì)象,這會(huì)非常麻煩。

因此MybatisPlus提供了很多特殊類(lèi)型字段的類(lèi)型處理器,解決特殊字段類(lèi)型與數(shù)據(jù)庫(kù)類(lèi)型轉(zhuǎn)換的問(wèn)題。例如處理JSON就可以使用JacksonTypeHandler處理器(SpringMVC底層默認(rèn)也是使用的這個(gè)類(lèi))。

(1)定義接收J(rèn)son的實(shí)體類(lèi)

首先定義一個(gè)單獨(dú)實(shí)體類(lèi)UserInfo來(lái)與info字段的屬性匹配

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")  // 為了方便構(gòu)建對(duì)象,為有參構(gòu)造提供靜態(tài)方法,名為of,UserInfo.of()
public class UserInfo {private Integer age;private String intro;private String gender;
}

(2)指定類(lèi)型處理器

UserUserVO類(lèi)的info字段修改為UserInfo類(lèi)型,并聲明類(lèi)型處理器@TableField(typeHandler = JacksonTypeHandler.class)。另外,將info改為對(duì)象類(lèi)型后出現(xiàn)對(duì)象嵌套,在復(fù)雜嵌套查詢(xún)時(shí)需要使用resultMap結(jié)果集映射,否則無(wú)法映射。所以還需要再@TableName注解中添加autoResultMap=true確保能夠正常映射。

@Data
@TableName(value = "user", autoResultMap = true)
public class User {/*** 用戶(hù)id*///@TableId(type = IdType.AUTO)private Long id;/*** 用戶(hù)名*/private String username;/*** 密碼*/private String password;/*** 注冊(cè)手機(jī)號(hào)*/private String phone;/*** 詳細(xì)信息*/@TableField(typeHandler = JacksonTypeHandler.class)private UserInfo info;/*** 使用狀態(tài)(1正常 2凍結(jié))*/private UserStatus status;/*** 賬戶(hù)余額*/private Integer balance;/*** 創(chuàng)建時(shí)間*/private LocalDateTime createTime;/*** 更新時(shí)間*/private LocalDateTime updateTime;
}

如果啟動(dòng)mapper.xml報(bào)錯(cuò),在info字段后加上, typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler就解決了。

  • 測(cè)試效果


6、yaml配置加密

目前我們配置文件中的很多參數(shù)都是明文,如果開(kāi)發(fā)人員發(fā)生流動(dòng),很容易導(dǎo)致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。

我們以數(shù)據(jù)庫(kù)的用戶(hù)名和密碼為例。

(1)生成密鑰

首先,我們利用MP提供的AES工具生成一個(gè)隨機(jī)秘鑰,然后對(duì)用戶(hù)名、密碼加密。

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 位隨機(jī) AES 密鑰String randomKey = AES.generateRandomKey();System.out.println("randomKey = " + randomKey); // randomKey = 7pSEa6F9TnYacTNJ// 利用密鑰對(duì)用戶(hù)名加密String username = AES.encrypt(USERNAME, randomKey);System.out.println("username = " + username);   // username = O4Yq+WKYGlPW5t8QvgrhUQ==// 利用密鑰對(duì)用戶(hù)名加密String password = AES.encrypt(PASSWORD, randomKey);System.out.println("password = " + password);   // password = cDYHnWysq07zUIAy1tcbRQ==}
}

(2)修改配置

修改application.yaml文件,把jdbc的用戶(hù)名、密碼修改為剛剛加密生成的密文。

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:開(kāi)頭password: mpw:cDYHnWysq07zUIAy1tcbRQ== # 密文要以 mpw:開(kāi)頭

(3)配置密鑰運(yùn)行參數(shù)

在啟動(dòng)項(xiàng)目的時(shí)候,需要把剛才生成的秘鑰添加到Jar啟動(dòng)參數(shù)中,像這樣:

--mpw.key=7pSEa6F9TnYacTNJ,新版本idea添加Program arguments中設(shè)置,界面如下

單元測(cè)試的時(shí)候不能添加啟動(dòng)參數(shù),所以要在測(cè)試類(lèi)的注解上配置:@SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ")

然后隨意運(yùn)行一個(gè)單元測(cè)試,可以發(fā)現(xiàn)數(shù)據(jù)庫(kù)查詢(xún)正常,以上就是給SpringBoot的application.yaml配置文件中的敏感重要數(shù)據(jù)加密的實(shí)現(xiàn)步驟。

(4)實(shí)現(xiàn)原理

SpringBoot提供修改Spring環(huán)境后置處理器【EnvironmentPostProcessor】,允許在應(yīng)用程序之前操作環(huán)境屬性值,MyBatisPlus對(duì)其進(jìn)行了重寫(xiě)實(shí)現(xiàn)。

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;}}/*** 處理加密內(nèi)容*/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));}}}}}// 將解密的數(shù)據(jù)放入環(huán)境變量,并處于第一優(yōu)先級(jí)上if (CollectionUtils.isNotEmpty(map)) {environment.getPropertySources().addFirst(new MapPropertySource("custom-encrypt", map));}}}
}

7、自動(dòng)填充字段

MyBatis-Plus提供了一個(gè)便捷的自動(dòng)填充功能,用于在插入或更新數(shù)據(jù)時(shí)自動(dòng)填充某些字段,如創(chuàng)建時(shí)間、更新時(shí)間等。

(1)配置自動(dòng)填充處理器

自動(dòng)填充功能通過(guò)實(shí)現(xiàn) com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 接口來(lái)實(shí)現(xiàn)。我們需要?jiǎng)?chuàng)建一個(gè)類(lèi)來(lái)實(shí)現(xiàn)這個(gè)接口,并在其中定義插入和更新時(shí)的填充邏輯。添加@Component配置自動(dòng)填充處理器類(lèi)被Spring管理。

  • MyMetaObjectHandler實(shí)現(xiàn)MetaObjectHandler接口
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {log.info("開(kāi)始插入填充...");// 起始版本 3.3.3(推薦)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// Date類(lèi)型填充//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("開(kāi)始更新填充...");// 起始版本 3.3.3(推薦)this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// Date類(lèi)型填充//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屬性

在實(shí)體類(lèi)中,你需要使用 @TableField 注解來(lái)標(biāo)記哪些字段需要自動(dòng)填充,并通過(guò)fill屬性指定填充的策略。

  • User實(shí)體類(lèi)
@Data
@TableName(value = "user", autoResultMap = true)
public class User {// 省略.../*** 創(chuàng)建時(shí)間*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 更新時(shí)間*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
  • FieldFill 枚舉類(lèi)
public enum FieldFill {DEFAULT,       // 默認(rèn)不處理INSERT,        // 插入填充字段UPDATE,        // 更新填充字段INSERT_UPDATE  // 插入和更新填充字段
}

注意事項(xiàng):

  1. 自動(dòng)填充是直接給實(shí)體類(lèi)的屬性設(shè)置值,如果屬性沒(méi)有值,入庫(kù)時(shí)會(huì)是null。
  2. MetaObjectHandler 提供的默認(rèn)方法策略是:如果屬性有值則不覆蓋,如果填充值為 null 則不填充。
  3. 字段必須聲明 @TableField 注解,并設(shè)置 fill 屬性來(lái)選擇填充策略。
  4. update(T entity, Wrapper<T> updateWrapper) 時(shí),entity 不能為空,否則自動(dòng)填充失效。
  5. update(Wrapper<T> updateWrapper) 時(shí)不會(huì)自動(dòng)填充,需要手動(dòng)賦值字段條件。
  6. 使用 strictInsertFillstrictUpdateFill 方法可以根據(jù)注解 FieldFill.xxx、字段名和字段類(lèi)型來(lái)區(qū)分填充邏輯。如果不需區(qū)分,可以使用 fillStrategy 方法。



五、插件功能

MybatisPlus提供了很多的插件功能,進(jìn)一步拓展其功能。目前已有的插件有:

  • PaginationInnerInterceptor:自動(dòng)分頁(yè)
  • TenantLineInnerInterceptor:多租戶(hù)
  • DynamicTableNameInnerInterceptor:動(dòng)態(tài)表名
  • OptimisticLockerInnerInterceptor:樂(lè)觀鎖
  • IllegalSQLInnerInterceptor:sql 性能規(guī)范
  • BlockAttackInnerInterceptor:防止全表更新與刪除

注意:使用多個(gè)分頁(yè)插件的時(shí)候需要注意插件定義順序,建議使用順序如下:

  • 多租戶(hù),動(dòng)態(tài)表名
  • 分頁(yè),樂(lè)觀鎖
  • sql 性能規(guī)范,防止全表更新與刪除

這里我們以最常用的分頁(yè)插件為例來(lái)學(xué)習(xí)插件的用法。


1、分頁(yè)插件

在未引入分頁(yè)插件的情況下,MybatisPlus是不支持分頁(yè)功能的,IServiceBaseMapper中的分頁(yè)方法都無(wú)法正常起效。 所以,我們必須配置分頁(yè)插件。

(1)引入依賴(lài)

? 注意,MyBatisPlus于 v3.5.9 起,PaginationInnerInterceptor已分離出來(lái)。如需使用,則需單獨(dú)引入mybatis-plus-jsqlparser依賴(lài)!

<!-- MP分頁(yè)插件 jdk 11+ 引入可選模塊 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.9</version>
</dependency>
<!-- MP分頁(yè)插件 jdk 8+ 引入可選模塊 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

(2)配置分頁(yè)內(nèi)置攔截器

在項(xiàng)目中新建一個(gè)配置類(lèi)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配置類(lèi)*/
@Configuration
public class MybatisConfig {/*** MP攔截器*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分頁(yè)插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(1000L);  // 設(shè)置單頁(yè)分頁(yè)條數(shù)最大限制interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}

(3)分頁(yè)API

  • 測(cè)試分頁(yè)查詢(xún)
@Test
void testPageQuery() {// 準(zhǔn)備分頁(yè)條件int pageNo = 1, pageSize = 2;   // 頁(yè)碼、每頁(yè)查詢(xún)條數(shù)// 查詢(xún)條件:無(wú)// 分頁(yè)條件Page<User> page = Page.of(pageNo, pageSize);// 排序條件//page.addOrder(new OrderItem("id", true)); // MP老版本排序條件寫(xiě)法,true為升序,false為降序page.addOrder(OrderItem.desc("balance"));   // 新版MP直接用OrderItem的靜態(tài)方法page.addOrder(OrderItem.asc("id")); // 可以添加多個(gè)排序條件// 分頁(yè)查詢(xún)page = userService.page(page);  // 這里返回的page對(duì)象其實(shí)和上面是同一個(gè)地址,只不過(guò)是封裝好分頁(yè)查詢(xún)結(jié)果的page對(duì)象// 獲取page中的查詢(xún)結(jié)果long total = page.getTotal();   // 總條數(shù)System.out.println("total = " + total);long pages = page.getPages();   // 總頁(yè)數(shù)System.out.println("pages = " + pages);List<User> records = page.getRecords(); // 分頁(yè)數(shù)據(jù)records.forEach(System.out::println);
}
  • 分頁(yè)查詢(xún)效果


2、通用分頁(yè)實(shí)體

現(xiàn)在要實(shí)現(xiàn)一個(gè)用戶(hù)分頁(yè)查詢(xún)的接口,接口規(guī)范如下:

參數(shù)說(shuō)明
請(qǐng)求方式GET
請(qǐng)求路徑/users/page
請(qǐng)求參數(shù)json { "pageNo": 1, "pageSize": 5, "sortBy": "balance", "isAsc": false, "name": "o", "status": 1 }
返回值json { "total": 100006, "pages": 50003, "list": [ { "id": 1685100878975279298, "username": "user_9****", "info": { "age": 24, "intro": "英文老師", "gender": "female" }, "status": "正常", "balance": 2000 } ] }
特殊說(shuō)明(1) 如果排序字段為空,默認(rèn)按照更新時(shí)間排序
(2) 排序字段不為空,則按照排序字段排序

這里需要用到4個(gè)實(shí)體:

  • PageDTO:通用分頁(yè)查詢(xún)條件的實(shí)體,包含分頁(yè)頁(yè)碼、每頁(yè)查詢(xún)條數(shù)、排序字段、是否升序
  • UserQueryDTO:接收用戶(hù)查詢(xún)條件實(shí)體,為了實(shí)現(xiàn)分頁(yè)功能去繼承PageDTO
  • PageResult:分頁(yè)結(jié)果實(shí)體,包含總條數(shù)、總頁(yè)數(shù)、當(dāng)前頁(yè)數(shù)據(jù)
  • UserVO:響應(yīng)用戶(hù)頁(yè)面視圖實(shí)體,將用戶(hù)VO集合封裝到PageResult中返回

(1)實(shí)體類(lèi)設(shè)計(jì)

分頁(yè)條件不僅僅用戶(hù)分頁(yè)查詢(xún)需要,以后其它業(yè)務(wù)也都有分頁(yè)查詢(xún)的需求。因此建議將分頁(yè)查詢(xún)條件單獨(dú)定義為一個(gè)PageDTO實(shí)體

  • PageDTO 通用分頁(yè)查詢(xún)條件的實(shí)體
@Data
@ApiModel(description = "通用分頁(yè)查詢(xún)實(shí)體")
public class PageDTO {@ApiModelProperty("頁(yè)碼")private Integer pageNo;@ApiModelProperty("每頁(yè)查詢(xún)條數(shù)")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;
}

PageDTO是接收前端提交的查詢(xún)參數(shù),一般包含四個(gè)屬性:

  • pageNo:頁(yè)碼
  • pageSize:每頁(yè)數(shù)據(jù)條數(shù)
  • sortBy:排序字段
  • isAsc:是否升序
  • UserQueryDTO 接收用戶(hù)查詢(xún)條件實(shí)體,繼承PageDTO
@EqualsAndHashCode(callSuper = true)    // 當(dāng)判斷相等時(shí)先考慮父類(lèi)屬性再考慮子類(lèi)屬性,就是分頁(yè)的時(shí)候把分頁(yè)條件作為數(shù)據(jù)請(qǐng)求的的前提,然后再考慮查到了哪些匹配的數(shù)據(jù)
@Data
@ApiModel(description = "用戶(hù)查詢(xún)條件實(shí)體")
public class UserQueryDTO extends PageDTO {@ApiModelProperty("用戶(hù)名關(guān)鍵字")private String name;@ApiModelProperty("用戶(hù)狀態(tài):1-正常,2-凍結(jié)")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
  • PageResult 分頁(yè)結(jié)果實(shí)體
@Data
@ApiModel(description = "分頁(yè)結(jié)果")
public class PageResult<T> {@ApiModelProperty("總條數(shù)")private Long total;@ApiModelProperty("總頁(yè)數(shù)")private Long pages;@ApiModelProperty("結(jié)果集合")private List<T> list;
}
  • UserVO 響應(yīng)用戶(hù)頁(yè)面視圖實(shí)體,將用戶(hù)VO集合封裝到PageResult中返回,之前已經(jīng)定義過(guò)了
@Data
@ApiModel(description = "用戶(hù)VO實(shí)體")
public class UserVO {@ApiModelProperty("用戶(hù)id")private Long id;@ApiModelProperty("用戶(hù)名")private String username;@ApiModelProperty("詳細(xì)信息")private UserInfo info;@ApiModelProperty("使用狀態(tài)(1正常 2凍結(jié))")private UserStatus status;@ApiModelProperty("賬戶(hù)余額")private Integer balance;@ApiModelProperty("用戶(hù)的收獲地址")private List<AddressVO> addresses;
}

(2)開(kāi)發(fā)接口

  • UserController中定義分頁(yè)條件查詢(xún)用戶(hù)的接口
// in UserController
@ApiOperation("根據(jù)條件分頁(yè)查詢(xún)用戶(hù)接口")
@GetMapping("/condition/page")
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {return userService.queryUserByConditionAndPage(queryDTO);
}
  • IUserService接口和UserServiceImpl實(shí)現(xiàn)類(lèi)
// in IUserService
PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO);// in UserServiceImpl
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 構(gòu)建分頁(yè)條件Page<User> page = Page.of(queryDTO.getPageNo(), queryDTO.getPageSize());// 構(gòu)建排序條件if (StrUtil.isNotBlank(queryDTO.getSortBy())) { // 如果排序字段不為空page.addOrder(new OrderItem().setColumn(queryDTO.getSortBy()).setAsc(queryDTO.getIsAsc()));}else { // 如果排序字段為空,默認(rèn)按照更新時(shí)間排序page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));}// 分頁(yè)查詢(xún)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結(jié)果PageResult<UserVO> result = new PageResult<>();result.setTotal(p.getTotal());  // 總條數(shù)result.setPages(p.getPages());  // 總頁(yè)數(shù)// 當(dāng)前頁(yè)數(shù)據(jù)List<User> records = p.getRecords();// 其實(shí)也可以不用判斷,因?yàn)槿绻榈降氖强占?#xff0c;轉(zhuǎn)換完還是空集合,不影響最后的結(jié)果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 將用戶(hù)集合拷貝為用戶(hù)VO集合List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);result.setList(userVOList);return result;
}
  • 測(cè)試根據(jù)條件和分頁(yè)查詢(xún)用戶(hù)接口

發(fā)送的分頁(yè)請(qǐng)求

響應(yīng)的分頁(yè)數(shù)據(jù)


(3)改造PageDTO實(shí)體

在剛才的代碼中,從PageDTOMybatisPlusPage之間轉(zhuǎn)換的過(guò)程還是比較麻煩的。

對(duì)于PageDTO構(gòu)建為MP的分頁(yè)對(duì)象的部分,我們完全可以在PageDTO這個(gè)實(shí)體內(nèi)部中定義一個(gè)轉(zhuǎn)換方法,簡(jiǎn)化開(kāi)發(fā)。

  • 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 = "通用分頁(yè)查詢(xún)實(shí)體")
public class PageDTO {@ApiModelProperty("頁(yè)碼")private Integer pageNo;@ApiModelProperty("每頁(yè)查詢(xún)條數(shù)")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;/*** 在PageDTO內(nèi)部將PageDTO構(gòu)建為MP的分頁(yè)對(duì)象,并設(shè)置排序條件* @param items 排序條件(可以有一個(gè)或多個(gè))* @return MP的分頁(yè)對(duì)象* @param <T> MP的分頁(yè)對(duì)象的泛型*/public <T> Page<T> toMpPage(OrderItem... items) {// 構(gòu)建分頁(yè)條件Page<T> page = Page.of(pageNo, pageSize);// 構(gòu)建排序條件if (sortBy != null && sortBy.trim().length() > 0) { // 如果排序字段不為空page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));}else if (items != null) { // 如果排序字段為空,且傳入的默認(rèn)OrderItem不為空,按照調(diào)用者傳入的默認(rèn)OrderItem排序page.addOrder(items);}return page;}/*** 將PageDTO構(gòu)建為MP的分頁(yè)對(duì)象* 如果調(diào)用者不想new OrderItem對(duì)象,可以調(diào)用該方法,傳入默認(rèn)的排序字段和排序規(guī)則即可* @param defaultSortBy 排序字段* @param defaultAsc 排序規(guī)則,true為asc,false為desc* @return MP的分頁(yè)對(duì)象* @param <T> MP的分頁(yè)對(duì)象的泛型*/public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {return toMpPage(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));}/*** 將PageDTO構(gòu)建為MP的分頁(yè)對(duì)象,排序條件按update_time更新時(shí)間降序* @return MP的分頁(yè)對(duì)象* @param <T> MP的分頁(yè)對(duì)象的泛型*/public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {return toMpPage("update_time", false);}/*** 將PageDTO構(gòu)建為MP的分頁(yè)對(duì)象,排序條件按create_time創(chuàng)建時(shí)間降序* @return MP的分頁(yè)對(duì)象* @param <T> MP的分頁(yè)對(duì)象的泛型*/public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {return toMpPage("create_time", false);}
}

這樣我們?cè)陂_(kāi)發(fā)也時(shí)就可以省去對(duì)從PageDTOPage的的轉(zhuǎn)換:

// 構(gòu)建分頁(yè)條件對(duì)象
Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();

(4)改造PageResult實(shí)體

在查詢(xún)出分頁(yè)結(jié)果后,數(shù)據(jù)的非空校驗(yàn),數(shù)據(jù)的VO轉(zhuǎn)換都是模板代碼,編寫(xiě)起來(lái)很麻煩。

我們完全可以將 PO分頁(yè)對(duì)象轉(zhuǎn)換為VO分頁(yè)結(jié)果對(duì)象 的邏輯,封裝到 PageResult 的內(nèi)部方法中,簡(jiǎn)化整個(gè)過(guò)程。

  • PageResult
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;@Data
@ApiModel(description = "分頁(yè)結(jié)果")
public class PageResult<T> {@ApiModelProperty("總條數(shù)")private Long total;@ApiModelProperty("總頁(yè)數(shù)")private Long pages;@ApiModelProperty("結(jié)果集合")private List<T> list;/*** 將PO分頁(yè)對(duì)象轉(zhuǎn)換為VO分頁(yè)結(jié)果對(duì)象* @param page PO分頁(yè)對(duì)象* @param clazz 目標(biāo)VO的字節(jié)碼對(duì)象* @return VO分頁(yè)結(jié)果對(duì)象* @param <PO> PO實(shí)體* @param <VO> VO實(shí)體*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Class<VO> clazz) {// PO分頁(yè)對(duì)象封裝為VO結(jié)果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal());  // 總條數(shù)result.setPages(page.getPages());  // 總頁(yè)數(shù)List<PO> records = page.getRecords();    // 當(dāng)前頁(yè)數(shù)據(jù)// 其實(shí)也可以不用判斷,因?yàn)槿绻榈降氖强占?#xff0c;轉(zhuǎn)換完還是空集合,不影響最后的結(jié)果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分頁(yè)對(duì)象轉(zhuǎn)換為VO分頁(yè)結(jié)果對(duì)象* @param page PO分頁(yè)對(duì)象* @param convertor 自定義規(guī)則轉(zhuǎn)換器* @return VO分頁(yè)結(jié)果對(duì)象* @param <PO> PO實(shí)體* @param <VO> VO實(shí)體*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Function<PO, VO> convertor) {// PO分頁(yè)對(duì)象封裝為VO結(jié)果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal());  // 總條數(shù)result.setPages(page.getPages());  // 總頁(yè)數(shù)List<PO> records = page.getRecords();    // 當(dāng)前頁(yè)數(shù)據(jù)// 其實(shí)也可以不用判斷,因?yàn)槿绻榈降氖强占?#xff0c;轉(zhuǎn)換完還是空集合,不影響最后的結(jié)果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 將PO集合轉(zhuǎn)換為VO集合,轉(zhuǎn)換動(dòng)作由調(diào)用者來(lái)傳遞List<VO> voList = records.stream().map(convertor).collect(Collectors.toList());result.setList(voList);return result;}
}

這兩個(gè)改造都相當(dāng)于定義通用業(yè)務(wù)工具類(lèi)。最終,業(yè)務(wù)層的代碼可以簡(jiǎn)化為:

@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 構(gòu)建分頁(yè)條件對(duì)象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分頁(yè)查詢(xún)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分頁(yè)對(duì)象封裝為VO結(jié)果return PageResult.of(p, UserVO.class);
}

如果是希望自定義PO到VO的轉(zhuǎn)換過(guò)程,可以調(diào)用重載方法of(Page<PO> page, Function<PO, VO> convertor),convertor的轉(zhuǎn)換器邏輯由調(diào)用者去編寫(xiě)傳遞:

@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 構(gòu)建分頁(yè)條件對(duì)象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分頁(yè)查詢(xún)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分頁(yè)對(duì)象封裝為VO結(jié)果return PageResult.of(p, user -> {// PO拷貝基礎(chǔ)屬性得到VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 對(duì)VO進(jìn)行處理特殊邏輯String username = userVO.getUsername();// 例如用戶(hù)名脫敏處理userVO.setUsername(username.length() > 2 ? StrUtil.fillAfter(username.substring(0, 2), '*', username.length()) : username.charAt(0) + "*");return userVO;});
}

自定義轉(zhuǎn)換規(guī)則的場(chǎng)景,例如:

  • ① PO字段和VO字段不是包含關(guān)系,出現(xiàn)字段不一致。
  • ② 對(duì)VO中的屬性做一些過(guò)濾、數(shù)據(jù)脫敏、加密等操作。
  • ③ 將VO中的屬性繼續(xù)設(shè)置數(shù)據(jù),例如VO中的address屬性,可以查詢(xún)出用戶(hù)所屬的收獲地址,設(shè)置后一并返回。

  • 最終的查詢(xún)結(jié)果如下




http://www.risenshineclean.com/news/61593.html

相關(guān)文章:

  • 中國(guó)文化網(wǎng)站建設(shè)策劃書(shū)網(wǎng)絡(luò)優(yōu)化
  • 網(wǎng)絡(luò)營(yíng)銷(xiāo)客戶(hù)的心里特征搜外seo
  • 網(wǎng)站數(shù)據(jù)庫(kù)在空間嗎網(wǎng)絡(luò)廣告四個(gè)特征
  • 政府網(wǎng)站內(nèi)容建設(shè)評(píng)估網(wǎng)站如何做seo推廣
  • 南京做網(wǎng)站seo免費(fèi)seo在線工具
  • 第一接單網(wǎng)平臺(tái)seo營(yíng)銷(xiāo)推廣
  • 漸變網(wǎng)站公眾號(hào)怎么推廣
  • 招商網(wǎng)站開(kāi)發(fā)文檔整站優(yōu)化案例
  • 門(mén)戶(hù)網(wǎng)站是什么意思啊線上推廣方式
  • wordpress導(dǎo)航調(diào)用代碼湖南專(zhuān)業(yè)的關(guān)鍵詞優(yōu)化
  • 公司網(wǎng)站網(wǎng)址注冊(cè)和備案哪里找sem數(shù)據(jù)分析
  • 北京開(kāi)放疫情最新消息關(guān)鍵詞優(yōu)化精靈
  • 網(wǎng)站做圖標(biāo)鏈接網(wǎng)站域名查詢(xún)地址
  • 表情包做舊網(wǎng)站百度競(jìng)價(jià)推廣賬戶(hù)
  • 網(wǎng)站建設(shè)藤設(shè)計(jì)廣告做到百度第一頁(yè)
  • 天津外貿(mào)網(wǎng)站建設(shè)公司如何優(yōu)化關(guān)鍵詞的方法
  • 公司做的網(wǎng)站入哪個(gè)會(huì)計(jì)科目怎么安裝百度
  • 建設(shè)個(gè)人商城網(wǎng)站seo優(yōu)化知識(shí)
  • 創(chuàng)業(yè)如何進(jìn)行網(wǎng)站建設(shè)路由優(yōu)化大師
  • 徐州建設(shè)工程交易網(wǎng)平臺(tái)官網(wǎng)網(wǎng)站seo搜索引擎優(yōu)化案例
  • 太原小程序開(kāi)發(fā)定制學(xué)校seo推廣培訓(xùn)班
  • 中央人民政府網(wǎng)站網(wǎng)址百度圖片
  • 西安專(zhuān)業(yè)網(wǎng)站建設(shè)公司開(kāi)創(chuàng)集團(tuán)與百度
  • wordpress 遠(yuǎn)程數(shù)據(jù)庫(kù)太原百度搜索排名優(yōu)化
  • 網(wǎng)站備案添加域名識(shí)圖找圖
  • 網(wǎng)站建設(shè)的優(yōu)點(diǎn)如何搭建自己的網(wǎng)站
  • 手把手教你做網(wǎng)站7seo推廣方案
  • 臨海市城鄉(xiāng)建設(shè)規(guī)劃局網(wǎng)站廣州搜發(fā)網(wǎng)絡(luò)科技有限公司
  • 萬(wàn)站群cms百度入口提交
  • 用國(guó)外服務(wù)器做網(wǎng)站網(wǎng)絡(luò)宣傳怎么做