一、MyBatis-Plus 简介

1.1 什么是 MyBatis-Plus

MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,由 baomidou 团队开发维护,核心设计理念是 “只做增强,不做改变”—— 完全兼容 MyBatis 原生功能,不侵入现有代码,同时封装了大量重复的单表 CRUD 操作,让开发者无需手写繁琐的 SQL 或 XML 映射文件,即可快速完成业务开发。

正如其 slogan “TO BE THE BEST PARTNER OF MYBATIS” 所传递的,MP 并非替代 MyBatis,而是作为其 “最佳搭档” 存在。PPT 中用 “润物无声,如丝般顺滑” 形容其特性,恰是因为引入后不会对现有 MyBatis 工程产生任何干扰,原有代码可无缝衔接。

1.2 核心特性

MP 的核心价值集中在 “简化开发” 与 “效率提升”,结合课程内容可归纳为以下 6 点:

  • 无侵入性:基于 MyBatis 原生架构扩展,不修改 MyBatis 核心逻辑,现有工程引入后无需重构代码。
  • 效率至上:内置 BaseMapper(DAO 层)和 IService(Service 层)接口,封装了全量单表 CRUD 操作(新增、删除、查询、更新等),实现 “零 SQL 开发”。
  • 智能映射:默认遵循 “驼峰 - 下划线” 转换规则(类名→表名、变量名→字段名),默认将id字段作为主键,减少手动配置成本。
  • 灵活条件构造:提供 QueryWrapperLambdaQueryWrapper 等条件构造器,支持动态拼接复杂 WHERE 条件,避免字符串拼接 SQL 的语法错误风险。
  • 丰富扩展功能:内置代码生成、逻辑删除、枚举字段映射、JSON 字段处理等高频场景工具,覆盖日常开发痛点。
  • 可插拔插件:支持分页、乐观锁、多租户、动态表名等插件,通过简单配置即可启用,扩展性极强。

1.3 支持的数据库

兼容所有 MyBatis 支持的关系型数据库,覆盖主流及企业级场景:

数据库类型 适配说明 典型使用场景
MySQL 适配 5.5+ / 8.0+ 版本 互联网应用、中小项目
Oracle 支持 11g+ 版本 企业级系统
SQL Server 兼容 2008+ 版本 微软技术栈项目
PostgreSQL 支持 9.4+ 版本,适配 JSON 字段 开源企业级项目
SQLite 适配 3.7+ 版本 轻量级应用、嵌入式系统
DB2 支持 10.5+ 版本 大型金融、政务系统

1.4 框架结构

MP 基于 MyBatis 分层设计,整体结构清晰,可拆解为 4 个核心层级:

  1. 底层依赖层:依赖 MyBatis 核心包、JDBC 驱动及 Spring 生态组件(如 Spring Boot Starter),确保与 Java 主流开发环境无缝集成。
  2. 核心功能层:MP 的 “心脏”,包含三大核心能力:
    • 通用 CRUD 接口:BaseMapper(提供单表 DAO 层操作)、IService(提供 Service 层批量操作、逻辑封装);
    • 实体映射:基于注解(@TableName@TableId 等)实现表与实体类的关联;
    • 条件构造:Wrapper 家族接口,动态生成 WHERE 条件。
  3. 扩展功能层:针对高频需求的封装,如代码生成器(自动生成 Entity、Mapper、Service 等代码)、逻辑删除(模拟删除效果,不真正删数据)、枚举 / JSON 处理器(解决特殊字段类型映射问题)。
  4. 插件层:基于 MyBatis 拦截器实现,提供分页(PaginationInnerInterceptor)、乐观锁(OptimisticLockerInnerInterceptor)、防止全表删除(BlockAttackInnerInterceptor)等插件,按需配置启用。

1.5 官方资源

二、MyBatis-Plus 快速入门

2.1 环境准备

2.1.1 数据库初始化

导入课前提供的mp.sql脚本,创建mp数据库及 3 张核心表,表结构与核心数据如下:

表名 作用 核心字段说明
user 核心用户表 id(主键)、username(用户名)、info(JSON 详情)、status(状态)、balance(余额)
address 用户地址表 iduser_id(外键关联 user.id)、province(省)、city(市)、is_default(默认地址)
tb_user 注解测试专用表 user_id(主键)、usernameis_deleted(逻辑删除标记)、order(排序字段)

核心表结构示例(user 表)

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `user` (
`id` BIGINT(19) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`password` VARCHAR(128) NOT NULL COMMENT '密码',
`phone` VARCHAR(20) NULL DEFAULT NULL COMMENT '注册手机号',
`info` JSON NOT NULL COMMENT '详细信息',
`status` INT(10) NULL DEFAULT '1' COMMENT '使用状态(1正常 2冻结)',
`balance` INT(10) NULL DEFAULT NULL COMMENT '账户余额',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username` (`username`) USING BTREE
) COMMENT='用户表';

2.1.2 项目导入与配置

  1. 导入项目:加载mp-demo项目结构,确保核心包路径为com.itheima.mp(含domain.pomapper等子包)。
  2. 配置数据库连接:修改application.yaml中的 JDBC 参数,适配本地数据库环境:
1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root # 替换为本地用户名
password: 123456 # 替换为本地密码
logging:
level:
com.itheima: debug # 打印SQL日志,便于调试
pattern:
dateformat: HH:mm:ss

2.2 MyBatis 项目改造为 MyBatis-Plus

基于现有 MyBatis 项目,通过 3 步改造即可实现user表的 CRUD 功能,核心是利用 MP 的BaseMapper省去手动编写 SQL 的工作。

2.2.1 步骤 1:替换依赖

MP 提供了 Spring Boot Starter,集成了 MyBatis 核心功能并实现自动装配,直接替换原 MyBatis 依赖:

Spring Boot 2.x 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- MyBatis-Plus Starter(替换mybatis-spring-boot-starter) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- 其他必要依赖 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

Spring Boot 3.x 依赖

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>

2.2.2 步骤 2:配置 Mapper 扫描

在 Spring Boot 启动类上添加@MapperScan注解,指定 Mapper 接口所在包:

1
2
3
4
5
6
7
8
9
10
11
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.itheima.mp.mapper") // 扫描Mapper接口
@SpringBootApplication
public class MpDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MpDemoApplication.class, args);
}
}

2.2.3 步骤 3:定义 Mapper 接口(核心改造)

MP 的BaseMapper接口已封装所有单表 CRUD 方法,只需让自定义 Mapper 继承它即可,无需编写 XML 映射文件。

改造前(MyBatis)

需手动定义 CRUD 方法及 XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Mapper接口
public interface UserMapper {
void saveUser(User user);
void deleteUser(Long id);
User queryUserById(@Param("id") Long id);
List<User> queryUserByIds(@Param("ids") List<Long> ids);
}

// 对应的XML映射文件(需编写大量SQL)
<?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">
<insert id="saveUser" parameterType="com.itheima.mp.domain.po.User">
INSERT INTO `user` (`id`, `username`, `password`) VALUES (#{id}, #{username}, #{password});
</insert>
<!-- 省略其他UPDATE/DELETE/SELECT标签 -->
</mapper>

改造后(MyBatis-Plus)

仅需继承BaseMapper,零 SQL 实现 CRUD:

1
2
3
4
5
6
7
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;

// 继承BaseMapper,泛型指定PO实体类
public interface UserMapper extends BaseMapper<User> {
// 无需手动写任何方法,BaseMapper已全部封装
}

2.2.4 改造效果测试

通过单元测试验证 MP 的 CRUD 能力,对比 MyBatis 实现,代码量大幅减少:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@SpringBootTest
class MyBatisPlusUserMapperTests {

@Autowired
private UserMapper userMapper;

// 新增用户
@Test
void testInsert() {
User user = new User();
user.setId(5L);
user.setUsername("Lucy");
user.setPassword("123");
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
userMapper.insert(user); // MP自带方法
}

// 根据ID查询
@Test
void testSelectById() {
User user = userMapper.selectById(5L); // MP自带方法
System.out.println("user = " + user);
}

// 批量查询
@Test
void testSelectByIds() {
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L)); // MP自带方法
users.forEach(System.out::println);
}

// 更新用户
@Test
void testUpdateById() {
User user = new User();
user.setId(5L);
user.setBalance(20000);
userMapper.updateById(user); // MP自带方法
}

// 删除用户
@Test
void testDelete() {
userMapper.deleteById(5L); // MP自带方法
}
}

SQL 日志示例(MP 自动生成标准 SQL,避免SELECT *):

1
2
3
11:54:23 DEBUG 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 c.i.mp.mapper.UserMapper.selectBatchIds : ==> Parameters: 1(Long), 2(Long), 3(Long)
11:54:23 DEBUG c.i.mp.mapper.UserMapper.selectBatchIds : <== Total: 3

2.3 常见注解

MP 通过反射推断 PO 与数据库表的映射关系,但默认约定(类名驼峰转表名、id为主键等)可能与实际不符,需通过注解手动声明映射规则。

2.3.1 核心注解说明

1. @TableName:指定表名

当 PO 类名与数据库表名不一致时使用,标识实体类对应的表。

属性 作用 示例
value 表名 @TableName("tb_user")
autoResultMap 自动构建 ResultMap(适配 JSON 等特殊字段) @TableName(value="tb_user", autoResultMap=true)

2. @TableId:指定主键

标识实体类的主键字段,支持自定义主键策略。

属性 作用 示例
value 主键字段名(与 PO 不一致时) @TableId("user_id")
type 主键生成策略 @TableId(type = IdType.AUTO)

主键策略(IdType 枚举核心值)

  • AUTO:数据库自增(需表主键设置自增)
  • ASSIGN_ID:雪花算法生成全局唯一 Long 型 ID(默认策略)
  • INPUT:手动设置主键值

3. @TableField:指定普通字段

解决字段名不一致、关键字冲突、非数据库字段等问题,常见场景:

场景 解决方案 示例
字段名不一致 指定value属性对应数据库字段 @TableField("username") private String name;
字段名含关键字(如order 用`转义 @TableField("order") private Integer order;
字段非数据库表字段 设置exist=false @TableField(exist = false) private String address;
布尔字段以is开头(如isDeleted 避免 JavaBean 解析丢失is @TableField("is_deleted") private Boolean isDeleted;

2.3.2 注解使用案例

tb_user表(注解测试表)为例,实现 PO 与表的完整映射:

1. 定义 PO 实体类(MpUser)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

@Data
@TableName("tb_user") // 指定表名为tb_user(与类名MpUser不一致)
public class MpUser {
// 主键:表字段为user_id,策略为数据库自增
@TableId(value = "user_id", type = IdType.AUTO)
private Long id;

// 普通字段:PO属性name对应表字段username
@TableField("username")
private String name;

// 普通字段:与表字段一致,无需注解
private String password;

// 布尔字段:isDeleted对应表字段is_deleted
@TableField("is_deleted")
private Boolean isDeleted;

// 关键字字段:order对应表字段`order`(转义)
@TableField("`order`")
private Integer order;

// 非数据库字段:不参与映射
@TableField(exist = false)
private String address;
}

2. 测试注解效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
public class MpUserMapperTests {
@Autowired
private MpUserMapper mpUserMapper;

@Test
void testInsert() {
MpUser mpUser = new MpUser();
mpUser.setName("MP用户1"); // 对应username字段
mpUser.setOrder(1); // 自动转义为`order`
mpUser.setAddress("测试地址"); // 非数据库字段,不插入
mpUserMapper.insert(mpUser);
}

@Test
void testSelectById() {
MpUser mpUser = mpUserMapper.selectById(1L);
System.out.println(mpUser); // address字段为null,不参与查询
}
}

2.4 常见配置

MP 支持通过application.yaml自定义全局配置,覆盖默认行为,核心配置项如下:

2.4.1 核心配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mybatis-plus:
# 1. 别名扫描:给PO包注册别名,XML中可直接用类名
type-aliases-package: com.itheima.mp.domain.po
# 2. Mapper XML路径:自定义SQL需配置(默认classpath*:/mapper/**/*.xml)
mapper-locations: "classpath*:/mapper/**/*.xml"
# 3. MyBatis原生配置
configuration:
map-underscore-to-camel-case: true # 驼峰-下划线映射(默认开启)
cache-enabled: false # 关闭二级缓存(按需开启)
# 4. MP全局配置
global-config:
db-config:
id-type: assign_id # 全局主键策略:雪花算法(默认)
update-strategy: not_null # 更新策略:只更新非null字段(默认)
table-prefix: tb_ # 表名前缀(如PO类User对应tb_user表,可省略@TableName)

2.4.2 关键配置说明

配置项 作用 默认值
type-aliases-package PO 类别名扫描包,XML 中可简化类名引用 无(需手动指定)
mapper-locations Mapper XML 文件路径,多模块用 classpath*: classpath*:/mapper/**/*.xml
map-underscore-to-camel-case 字段驼峰(PO)与下划线(表)自动映射 true(建议保持开启)
id-type 全局主键策略 assign_id(雪花算法)
update-strategy 更新字段策略 not_null(只更非 null 字段)
table-prefix 表名前缀,统一添加避免重复注解

2.4.3 配置优先级

局部注解 > 全局配置,例如:全局配置id-type: auto,但某 PO 的主键注解为@TableId(type = IdType.ASSIGN_ID),则该 PO 优先使用雪花算法。

三、MyBatis-Plus 核心功能

在基础 CRUD 操作之上,MyBatis-Plus 提供了三大核心能力来适配复杂业务场景:条件构造器解决动态筛选问题,自定义 SQL 适配特殊逻辑与多表场景,IService 接口封装 Service 层模板方法。三者结合可大幅提升开发效率,同时保留灵活性。

3.1 条件构造器:动态 SQL 优雅实现

基础 CRUD 依赖 ID 作为条件,但实际业务中往往需要更复杂的筛选逻辑(如多字段组合查询、范围查询等)。MyBatis-Plus 提供的 条件构造器(Wrapper) 正是为解决这类问题而生,它能动态拼接 WHERE 条件,无需手动编写 SQL 语句。

3.1.1 Wrapper 体系结构

Wrapper 是条件构造的核心抽象类,其设计采用了分层继承模式,不同子类对应不同场景的条件构造需求。

继承关系

image.png

核心类作用

  • AbstractWrapper:提供所有 WHERE 条件的基础方法(如 eq 等于、like 模糊查询、ge 大于等于等)。

  • QueryWrapper:在 AbstractWrapper 基础上增加 select() 方法,可指定查询字段(避免 SELECT *)。

  • UpdateWrapper:在 AbstractWrapper 基础上增加 set()setSql() 方法,支持复杂更新(如基于字段现有值的计算)。

  • LambdaXXXWrapper:通过 Lambda 表达式引用实体类方法(如 User::getUsername),避免直接写字符串字段名,减少拼写错误。

3.1.2 QueryWrapper:查询与简单更新 / 删除

QueryWrapper 是最常用的条件构造器,适用于 查询、删除、简单更新 场景,可通过链式调用拼接复杂 WHERE 条件。

1. 复杂查询示例

需求:查询用户名中包含 “o”、余额大于等于 1000 元的用户,只返回 id、username、info、balance 字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void testQueryWrapperSelect() {
// 1. 构建查询条件:WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 指定查询字段(避免SELECT *)
queryWrapper.select("id", "username", "info", "balance")
// 模糊查询:username LIKE "%o%"
.like("username", "o")
// 大于等于:balance >= 1000
.ge("balance", 1000);

// 2. 执行查询
List<User> userList = userMapper.selectList(queryWrapper);
userList.forEach(System.out::println);
}

生成的 SQL

1
2
3
4
SELECT id,username,info,balance 
FROM user
WHERE username LIKE ? AND balance >= ?
-- 参数:%o%(String)、1000(Integer)

2. 基于条件的更新

需求:将用户名为 “Jack” 的用户余额更新为 2000 元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
void testQueryWrapperUpdate() {
// 1. 准备更新数据(非null字段会被设置到SET中)
User user = new User();
user.setBalance(2000);

// 2. 构建条件:WHERE username = "Jack"
QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
.eq("username", "Jack"); // 等于条件

// 3. 执行更新(WHERE条件由queryWrapper决定,SET字段由user非null值决定)
int rows = userMapper.update(user, queryWrapper);
System.out.println("更新成功:" + (rows > 0));
}

生成的 SQL

1
2
3
4
UPDATE user 
SET balance=?
WHERE username = ?
-- 参数:2000(Integer)、Jack(String)

3. 基于条件的删除

需求:删除状态为 “冻结”(status=2)且余额为 0 的用户。

1
2
3
4
5
6
7
8
9
10
11
@Test
void testQueryWrapperDelete() {
// 1. 构建条件:WHERE status = 2 AND balance = 0
QueryWrapper<User> queryWrapper = new QueryWrapper<>()
.eq("status", 2)
.eq("balance", 0);

// 2. 执行删除
int rows = userMapper.delete(queryWrapper);
System.out.println("删除成功:" + (rows > 0));
}

生成的 SQL

1
2
3
DELETE FROM user 
WHERE status = ? AND balance = ?
-- 参数:2(Integer)、0(Integer)

3.1.3 UpdateWrapper:复杂更新场景

当更新操作需要 基于字段现有值计算(如 balance = balance - 200)时,QueryWrapper 无法满足需求,需使用 UpdateWrapper 的 setSql() 方法直接编写 SET 语句片段。

1. 基于字段现有值的更新

需求:给 id 为 1、2、4 的用户余额扣除 200 元(balance = balance - 200)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void testUpdateWrapper() {
// 1. 准备条件:WHERE id IN (1, 2, 4)
List<Long> ids = List.of(1L, 2L, 4L);

// 2. 构建更新条件与SET语句
UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
// 直接编写SET片段:balance = balance - 200
.setSql("balance = balance - 200")
// WHERE条件:id IN (1,2,4)
.in("id", ids);

// 3. 执行更新(无需传递User对象,SET语句由updateWrapper定义)
int rows = userMapper.update(null, updateWrapper);
System.out.println("更新成功:" + (rows > 0));
}

生成的 SQL

1
2
3
4
UPDATE user 
SET balance = balance - 200
WHERE id IN (?, ?, ?)
-- 参数:1(Long)、2(Long)、4(Long)

2. 组合更新(SET 多字段)

需求:给 “Jack” 的余额增加 500 元,同时将状态改为 “正常”(status=1)。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testUpdateWrapperComplex() {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
// SET balance = balance + 500, status = 1
.setSql("balance = balance + 500")
.set("status", 1) // 简单赋值也可使用set()
// WHERE username = "Jack"
.eq("username", "Jack");

int rows = userMapper.update(null, updateWrapper);
System.out.println("更新成功:" + (rows > 0));
}

生成的 SQL

1
2
3
4
UPDATE user 
SET balance = balance + 500, status = ?
WHERE username = ?
-- 参数:1(Integer)、Jack(String)

3.1.4 Lambda 条件构造器:避免硬编码

普通 Wrapper 在构造条件时需要手动输入字段名(如 "username"),存在 字符串硬编码 问题(拼写错误不会在编译期报错)。Lambda 条件构造器通过 方法引用 解决这一问题,让字段名与实体类属性强关联。

1. LambdaQueryWrapper 示例

需求:查询年龄大于 20 岁(从 info 字段的 JSON 中解析)、状态为正常(status=1)的用户。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testLambdaQueryWrapper() {
// 1. 构建Lambda查询条件(使用User::getXxx方法引用)
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// WHERE status = 1 AND username LIKE "%o%"
lambdaQueryWrapper.eq(User::getStatus, 1)
.like(User::getUsername, "o");

// 2. 执行查询
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(System.out::println);
}

优势:若误写为 User::getUsernam(少个 e),编译期会直接报错,避免运行时因字段名错误导致的 SQL 异常。

2. LambdaUpdateWrapper 示例

需求:将 id 为 3 的用户余额翻倍(balance = balance * 2),并更新修改时间。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testLambdaUpdateWrapper() {
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
// SET balance = balance * 2, update_time = NOW()
lambdaUpdateWrapper.setSql("balance = balance * 2")
.set(User::getUpdateTime, LocalDateTime.now())
// WHERE id = 3
.eq(User::getId, 3L);

int rows = userMapper.update(null, lambdaUpdateWrapper);
System.out.println("更新成功:" + (rows > 0));
}

生成的 SQL

1
2
3
4
UPDATE user 
SET balance = balance * 2, update_time = ?
WHERE id = ?
-- 参数:2024-11-13T15:30:00(LocalDateTime)、3(Long)

3.1.5 常用条件方法速查表

AbstractWrapper 提供了丰富的条件方法,覆盖 SQL 中所有常见的 WHERE 子句语法,以下是高频使用的方法:

方法名 作用 示例 对应的 SQL 片段
eq 等于(=) eq("name", "Jack") name = 'Jack'
ne 不等于(≠) ne("status", 0) status != 0
like 模糊查询(% 值 %) like("username", "o") username LIKE '%o%'
likeLeft 左模糊(% 值) likeLeft("phone", "139") phone LIKE '%139'
likeRight 右模糊(值 %) likeRight("email", "test") email LIKE 'test%'
gt 大于(>) gt("balance", 1000) balance > 1000
ge 大于等于(≥) ge("age", 18) age >= 18
lt 小于(<) lt("score", 60) score < 60
le 小于等于(≤) le("level", 3) level <= 3
in 包含在集合中 in("id", 1, 2, 3) id IN (1,2,3)
notIn 不包含在集合中 notIn("status", 0, 2) status NOT IN (0,2)
between 在范围内(闭区间) between("create_time", "2023-01-01", "2023-12-31") create_time BETWEEN '2023-01-01' AND '2023-12-31'
isNull 字段为 NULL isNull("email") email IS NULL
isNotNull 字段不为 NULL isNotNull("phone") phone IS NOT NULL
and 拼接 AND 条件(嵌套) and(w -> w.gt("balance", 1000).lt("balance", 5000)) AND (balance > 1000 AND balance < 5000)
or 拼接 OR 条件(嵌套) or(w -> w.eq("status", 1).or().eq("status", 3)) OR (status = 1 OR status = 3)

3.1.6 条件构造器使用总结

  1. 场景选择
    • 简单查询 / 更新 / 删除:优先使用 LambdaQueryWrapper(避免硬编码)。
    • 复杂更新(如基于字段计算):使用 LambdaUpdateWrapper
    • 特殊场景(如多表联查需手动写 SQL):可混合使用 Wrapper 和 XML 映射。
  2. 最佳实践
    • 始终优先使用 Lambda 版本(LambdaQueryWrapper/LambdaUpdateWrapper),减少字段名拼写错误。
    • 复杂条件使用 and()/or() 嵌套,避免长链式调用导致的可读性问题。
    • 多表查询时,Wrapper 仅负责单表条件,关联条件仍需在 XML 中编写。

通过条件构造器,MyBatis-Plus 实现了动态 SQL 的优雅处理,既保留了 MyBatis 的灵活性,又简化了重复代码,大幅提升了复杂查询场景的开发效率。

3.2 自定义 SQL 与 IService 接口:分层开发效率提升

在基础 CRUD 和条件构造的基础上,MyBatis-Plus 进一步通过 自定义 SQL 解决复杂场景(如多表联查、特殊更新),通过 IService 接口 封装 Service 层模板方法,实现 “分层开发” 与 “效率提升” 的结合。

3.2.1 自定义 SQL:适配特殊逻辑与多表场景

MyBatis-Plus 虽能覆盖大部分单表场景,但面对 特殊 SQL 逻辑(如字段自增 / 自减)或 多表联查 时,需结合 “Wrapper 生成条件 + 自定义 SQL 片段” 的方式,既保留动态条件的灵活性,又将 SQL 维护在持久层。

1. 核心场景:避免 SQL 硬编码到业务层

问题引出

使用 UpdateWrapper 时,若直接在 Service 层编写 setSql("balance = balance - 200"),会导致 SQL 片段散落在业务层,违背 “持久层维护 SQL” 的开发规范。此外,复杂条件(如动态 IN 语句)若纯手写 XML,需编写繁琐的 <foreach> 标签,易出错。

解决方案:Wrapper + 自定义 SQL 片段

MyBatis-Plus 提供 ew.customSqlSegment 变量,可直接引用 Wrapper 生成的 WHERE 条件片段,实现 “条件由 MP 构建,SQL 核心逻辑由自定义实现”。

实现步骤
  1. Service 层构建条件:用 Wrapper 定义查询条件(避免业务层写 SQL);
  2. Mapper 层声明方法:用 @Param("ew") 接收 Wrapper(参数名固定为ew,可用Constants.WRAPPER常量替代);
  3. 编写自定义 SQL:通过 ${ew.customSqlSegment} 嵌入 MP 生成的条件。

代码示例:字段自减更新

需求:批量扣减指定 ID 用户的余额(balance = balance - #{amount}),条件由 MP 构建。

1. Service 层:构建条件
1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductBalanceBatch(List<Long> ids, int amount) {
// 1. 用Lambda构建条件:WHERE id IN (ids)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.in(User::getId, ids);
// 2. 调用自定义SQL方法(条件+参数传入Mapper)
baseMapper.updateBalanceByIds(wrapper, amount);
}
}
2. Mapper 层:自定义 SQL 片段

支持 注解式 SQLXML 式 SQL,核心是引用 ew.customSqlSegment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface UserMapper extends BaseMapper<User> {
// 注解式SQL:${ew.customSqlSegment} 嵌入WHERE条件
@Update("UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}")
void updateBalanceByIds(
@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper, // 固定参数名ew
@Param("amount") int amount
);

// 若用XML式SQL,在userMapper.xml中编写:
// <update id="updateBalanceByIds">
// UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}
// </update>
}
3. 生成的 SQL
1
2
UPDATE user SET balance = balance - 200 WHERE id IN (?, ?, ?)
-- 参数:200(amount)、1(id)、2(id)、4(id)

2. 进阶场景:多表联查

MyBatis-Plus 本身不支持多表联查,但可通过 “自定义关联逻辑 + Wrapper 条件” 实现 ——关联部分手写 SQL,筛选条件由 MP 构建,大幅简化动态条件的编写。

需求:查询 “收货地址在北京” 且 “用户 ID 在 1、2、4 中” 的用户信息。

1. Service 层:构建多表条件
1
2
3
4
5
6
7
8
@Override
public List<User> queryUserWithAddr(String city, List<Long> userIds) {
// 构建条件:WHERE u.id IN (1,2,4) AND a.city = '北京'
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", userIds) // 表别名需与SQL一致
.eq("a.city", city);
return baseMapper.queryUserByAddr(wrapper);
}
2. Mapper 层:自定义多表 SQL
1
2
3
4
5
6
7
8
9
10
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper extends BaseMapper<User> {
// 关联查询:SELECT + JOIN 手写,WHERE条件用MP生成
@Select("SELECT u.* FROM user u " +
"INNER JOIN address a ON u.id = a.user_id " +
"${ew.customSqlSegment}")
List<User> queryUserByAddr(@Param("ew") QueryWrapper<User> wrapper);
}
3. 生成的 SQL
1
2
3
4
SELECT u.* FROM user u 
INNER JOIN address a ON u.id = a.user_id
WHERE u.id IN (?, ?, ?) AND a.city = ?
-- 参数:1(userIds)、2(userIds)、4(userIds)、北京(city)

3. 自定义 SQL 适用场景总结

场景 解决方案 优势
特殊更新(字段自增 / 自减) Wrapper 构建 WHERE + 自定义 SQL 写 SET 避免 SQL 散落在 Service 层
复杂动态条件(多字段筛选) Wrapper 生成条件 + 自定义 SQL 写核心逻辑 替代繁琐的<if>/<foreach>标签
多表联查 自定义 JOIN + Wrapper 生成筛选条件 保留 MP 动态条件优势,适配多表场景

3.2.2 IService 接口:Service 层模板方法封装

MyBatis-Plus 不仅在 DAO 层提供 BaseMapper,还在 Service 层封装了 IService 接口及默认实现,涵盖批量操作、条件查询、分页等高频模板方法,避免重复编写 Service 层代码。

1. IService 核心设计

  • 顶层接口IService<T>,定义 Service 层通用方法;
  • 默认实现ServiceImpl<M extends BaseMapper<T>, T>,已实现 IService 所有方法,内部依赖 BaseMapper
  • 使用方式:自定义 Service 接口继承 IService,实现类继承 ServiceImpl,即可复用所有模板方法。

2. 核心方法分类(高频)

IService 方法按功能可分为 7 大类,覆盖绝大多数 Service 层场景:

类别 核心方法 作用描述
新增 save(T entity) 新增单个实体
saveBatch(Collection<T> list) 批量新增(默认批次 1000 条)
saveOrUpdate(T entity) 新增或更新(根据 ID 判断)
删除 removeById(Serializable id) 根据 ID 删除
removeByIds(Collection<?> ids) 批量删除
remove(Wrapper<T> wrapper) 根据条件删除
更新 updateById(T entity) 根据 ID 更新(只更非 null 字段)
update(T entity, Wrapper<T> wrapper) 根据条件更新指定字段
updateBatchById(Collection<T> list) 批量更新
单条查询 getById(Serializable id) 根据 ID 查询
getOne(Wrapper<T> wrapper) 根据条件查询单条(默认抛异常 if 多条)
列表查询 list() 查询所有
listByIds(Collection<?> ids) 批量查询
list(Wrapper<T> wrapper) 根据条件查询列表
计数 count() 统计总条数
count(Wrapper<T> wrapper) 根据条件统计
分页 page(IPage<T> page, Wrapper<T> wrapper) 条件分页查询

3. 基本使用流程

步骤 1:定义自定义 Service 接口

继承 IService<T>,泛型为 PO 实体类:

1
2
3
4
5
6
7
8
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

// 自定义接口:继承IService,可添加业务方法
public interface IUserService extends IService<User> {
// 示例:自定义业务方法(扣减余额)
void deductBalanceById(Long id, Integer money);
}
步骤 2:实现 Service 类

继承 ServiceImpl<M, T>(M 为 Mapper 接口,T 为 PO),实现自定义接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
// 无需手动实现IService的方法,ServiceImpl已全部封装
// 可直接使用baseMapper调用自定义SQL,或使用IService的模板方法

@Override
public void deductBalanceById(Long id, Integer money) {
// 1. 复用IService的查询方法
User user = getById(id);
// 2. 业务校验
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常");
}
if (user.getBalance() < money) {
throw new RuntimeException("余额不足");
}
// 3. 调用自定义SQL(通过baseMapper)
baseMapper.deductBalance(id, money);
}
}
步骤 3:Controller 层调用

直接注入自定义 Service,复用模板方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;

// 新增用户:复用IService的save方法
@PostMapping
public void save(@RequestBody UserFormDTO dto) {
User user = BeanUtil.copyProperties(dto, User.class);
userService.save(user);
}

// 批量查询:复用IService的listByIds方法
@GetMapping
public List<UserVO> listByIds(@RequestParam List<Long> ids) {
List<User> users = userService.listByIds(ids);
return BeanUtil.copyToList(users, UserVO.class);
}

// 自定义业务:扣减余额
@PutMapping("/{id}/deduction/{money}")
public void deduct(@PathVariable Long id, @PathVariable Integer money) {
userService.deductBalanceById(id, money);
}
}

4. 进阶:LambdaQuery 与 LambdaUpdate(高效!)

IService 提供 lambdaQuery()lambdaUpdate() 方法(究极好用!!!),可直接链式构建 Lambda 条件,无需手动创建 LambdaQueryWrapper,进一步简化代码。

场景 1:动态条件查询

需求:根据 “用户名关键字、状态、余额范围” 动态查询用户(条件可为空)。

1. 定义查询 DTO
1
2
3
4
5
6
7
@Data
public class UserQueryDTO {
private String name; // 用户名关键字(可为空)
private Integer status; // 状态(可为空)
private Integer minBalance; // 最小余额(可为空)
private Integer maxBalance; // 最大余额(可为空)
}
2. Service 层实现
1
2
3
4
5
6
7
8
9
10
@Override
public List<User> queryByCondition(UserQueryDTO dto) {
// lambdaQuery():直接获取Lambda查询器,支持条件判断
return lambdaQuery()
.like(dto.getName() != null, User::getUsername, dto.getName()) // 非空才加条件
.eq(dto.getStatus() != null, User::getStatus, dto.getStatus())
.ge(dto.getMinBalance() != null, User::getBalance, dto.getMinBalance())
.le(dto.getMaxBalance() != null, User::getBalance, dto.getMaxBalance())
.list(); // 结尾指定返回类型(list/one/count)
}
场景 2:动态更新

需求:扣减余额,若扣减后余额为 0,则冻结用户(status=2)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
@Transactional // 事务保证
public void deductBalanceWithFreeze(Long id, Integer money) {
// 1. 查询用户
User user = getById(id);
// 2. 校验
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户异常");
}
int remain = user.getBalance() - money;
if (remain < 0) {
throw new RuntimeException("余额不足");
}
// 3. 动态更新:余额必更,状态按需更新
lambdaUpdate()
.set(User::getBalance, remain)
.set(remain == 0, User::getStatus, 2) // 余额为0则冻结
.eq(User::getId, id)
.update(); // 执行更新
}

5. 性能优化:批量新增与批处理

IServicesaveBatch() 方法默认采用 “预编译批处理”,但需配合 MySQL 驱动参数才能达到最优性能。

三种批量插入方案对比
方案 实现方式 耗时(10 万条数据) 原理
普通 for 循环逐条插入 for (User u : list) { save(u); } ~550 秒 每条数据 1 次网络请求,效率极低
MP 批量插入(默认) saveBatch(list, 1000) ~28 秒 每 1000 条 1 次网络请求,仍逐条执行 SQL
MP+MySQL 批处理参数 saveBatch(list, 1000) + rewriteBatchedStatements=true ~6 秒 驱动重写 SQL 为批量插入,1 次请求 1 条 SQL
关键配置:开启 MySQL 批处理

在 JDBC URL 中添加 rewriteBatchedStatements=true(MySQL 驱动 3.1.13 + 支持):

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: root
password: 123456
注意事项
  • 批次大小建议 1000-5000 条:避免单次请求数据量过大导致超时;
  • 内存优化:分批次构建数据列表,避免一次性创建 10 万条对象占用过多内存。

6. IService 使用总结

  1. 核心价值:封装 Service 层模板方法,减少重复代码(如批量操作、条件查询);
  2. 最佳实践
    • 自定义 Service 接口继承 IService,实现类继承 ServiceImpl
    • 简单 CRUD 直接复用 IService 方法,复杂业务在实现类中扩展;
    • 动态条件优先使用 lambdaQuery()/lambdaUpdate(),避免硬编码;
    • 批量操作务必开启 rewriteBatchedStatements 参数优化性能。

四、MyBatis-Plus 扩展功能

MyBatis-Plus 除核心 CRUD 能力外,还提供了一系列 高频场景扩展功能,覆盖代码生成、类型转换、敏感配置加密等开发痛点,旨在进一步简化开发流程、提升系统健壮性。

4.1 代码生成:一键生成重复代码

基础的 PO、Mapper、Service 等代码存在大量重复编写工作,MyBatis-Plus 提供两种代码生成方案:Idea 图形化插件(便捷)和 官方代码生成器(灵活),可根据数据库表结构自动生成全套基础代码。

4.1.1 方案一:Idea 插件生成(推荐新手)

通过 Idea 插件实现图形化配置,无需编写生成逻辑,直接生成代码。

1. 安装插件

在 Idea 的 Plugins 市场搜索 MyBatisPlus,选择下载量高的插件(如 “mybatis-plus” 或 “Custom MybatisPlus Generator”),安装后重启 Idea。

2. 使用步骤

以生成 address 表的基础代码为例:

  1. 配置数据库连接:顶部菜单选择 OtherConfig Database,填写数据库 URL、用户名、密码,测试连接通过后保存。

  2. 配置生成参数:再次选择 OtherCode Generator,填写核心参数:

    • package:父包路径(如 com.itheima.mp);
    • Entity:实体类包路径(如 domain.po);
    • TablePrefix:表前缀(如无则留空);
    • Id策略:与数据库主键策略一致(如 AUTO 自增);
    • 勾选需生成的组件(Entity、Mapper、Service、Controller 等)。

    img

  3. 生成代码:点击 “提交”,插件自动在指定包路径下生成代码,包含 PO 实体、Mapper 接口、Service 及实现类。

4.1.2 方案二:官方代码生成器(灵活定制,可跳过)

通过编写 Java 代码配置生成逻辑,支持个性化定制(如 Lombok 注解、REST 风格 Controller 等),适合复杂场景。

因为MP代码生成更新迭代速度很快,若本文的API被弃用,请以官网最新版本API为准:

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

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

1. 引入依赖

pom.xml 中添加代码生成器依赖:

1
2
3
4
5
6
<!-- MP代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.9</version>
</dependency>

2. 编写生成配置

创建 CodeGenerator 类,配置数据库连接、包路径、生成策略等参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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 = "jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8";
private static final String JDBC_USER = "root";
private static final String JDBC_PWD = "123456";

public static void main(String[] args) {
// 生成表名(可批量)
List<String> tables = List.of("address");
// 输出路径(项目java目录)
String outputDir = System.getProperty("user.dir") + "/src/main/java";

FastAutoGenerator.create(JDBC_URL, JDBC_USER, JDBC_PWD)
// 1. 全局配置
.globalConfig(builder -> {
builder.author("Aizen") // 作者
.outputDir(outputDir) // 输出路径
.enableSwagger() // 开启Swagger注解
.commentDate("yyyy-MM-dd"); // 注释日期格式
})
// 2. 包配置
.packageConfig(builder -> {
builder.parent("com.itheima.mp") // 父包名
.entity("domain.po") // 实体类包
.mapper("mapper") // Mapper接口包
.service("service") // Service接口包
.serviceImpl("service.impl") // Service实现包
.controller("controller") // Controller包
.xml("src/main/resources/mapper"); // Mapper XML路径
})
// 3. 策略配置
.strategyConfig(builder -> {
builder.addInclude(tables) // 要生成的表
.addTablePrefix() // 表前缀(如无则不填)
// 实体类策略
.entityBuilder()
.enableLombok() // 启用Lombok
.enableTableFieldAnnotation() // 生成字段注解
// Service策略
.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl")
// Controller策略
.controllerBuilder()
.enableRestStyle() // 启用REST风格
// Mapper策略
.mapperBuilder()
.superClass(BaseMapper.class) // 继承BaseMapper
.enableBaseResultMap(); // 生成通用ResultMap
})
.execute(); // 执行生成
}
}

3. 执行生成

运行 CodeGenerator.main() 方法,代码会自动生成到指定包路径,无需手动编写重复代码。

4.2 静态工具 Db:解决 Service 循环依赖

当 Service 之间相互调用时,易出现 循环依赖 问题。MyBatis-Plus 提供静态工具类 Db,封装了与 IService 一致的 CRUD 方法,无需注入 Service 即可实现数据操作,从根源避免循环依赖。

4.2.1 核心特性

  • 无需注入 Service,通过静态方法直接调用;
  • 需传入 PO 类的 Class 字节码(静态方法无法读取泛型,通过反射获取表信息);
  • 方法签名与 IService 基本一致,学习成本低。

4.2.2 基础使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.baomidou.mybatisplus.core.toolkit.Db;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class DbToolTest {

// 1. 根据ID查询
@Test
void testDbGetById() {
User user = Db.getById(1L, User.class); // 需传入PO的Class
System.out.println("用户:" + user);
}

// 2. 条件查询
@Test
void testDbLambdaQuery() {
List<User> userList = Db.lambdaQuery(User.class)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000)
.list();
userList.forEach(System.out::println);
}

// 3. 更新操作
@Test
void testDbLambdaUpdate() {
boolean success = Db.lambdaUpdate(User.class)
.set(User::getBalance, 2000)
.eq(User::getUsername, "Rose")
.update();
System.out.println("更新成功:" + success);
}
}

4.2.3 实战案例:查询用户及关联地址

案例 1:查询单个用户及地址

需求:查询用户时返回其所有收货地址,避免注入 IAddressService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public UserVO queryUserAndAddressById(Long id) {
// 1. 查询用户(自身Service方法)
User user = getById(id);
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户不存在或冻结");
}

// 2. 查询地址(用Db工具,无需注入AddressService)
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, id)
.list();

// 3. 封装VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
return userVO;
}
}

案例 2:批量查询用户及地址

需求:批量查询用户时,批量关联地址并分组匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1. 批量查询用户
List<User> users = listByIds(ids);
if (users.isEmpty()) return Collections.emptyList();

// 2. 批量查询所有用户的地址(一次查询,减少数据库交互)
List<Long> userIds = users.stream().map(User::getId).toList();
List<Address> addresses = Db.lambdaQuery(Address.class)
.in(Address::getUserId, userIds)
.list();

// 3. 地址按用户ID分组(Map<用户ID, 地址列表>)
Map<Long, List<AddressVO>> addressMap = addresses.stream()
.map(addr -> BeanUtil.copyProperties(addr, AddressVO.class))
.collect(Collectors.groupingBy(AddressVO::getUserId));

// 4. 封装VO
return users.stream()
.map(user -> {
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
vo.setAddresses(addressMap.getOrDefault(user.getId(), Collections.emptyList()));
return vo;
})
.toList();
}

4.3 逻辑删除:避免数据物理删除

对于核心数据(如用户、订单),物理删除(DELETE)会导致数据丢失,通常采用 逻辑删除 方案:通过字段标记数据状态,删除时仅修改标记,查询时过滤已标记的数据。MyBatis-Plus 可自动处理逻辑删除的 SQL 拼接,无需手动修改 CRUD 方法。

4.3.1 实现步骤

1. 数据库添加逻辑删除字段

address 表为例,添加 deleted 字段(布尔型,默认 0 未删除):

1
ALTER TABLE address ADD deleted BIT DEFAULT b'0' NULL COMMENT '逻辑删除(0未删,1已删)';

2. 配置逻辑删除

两种配置方式:全局配置(适用于全表统一规则)或 注解配置(适用于单表自定义规则)。

方式 1:全局配置(推荐)

application.yaml 中配置全局逻辑删除规则:

1
2
3
4
5
6
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-not-delete-value: 0 # 未删除值(默认0)
logic-delete-value: 1 # 已删除值(默认1)
方式 2:注解配置(单表自定义)

在 PO 类的逻辑删除字段上添加 @TableLogic 注解,覆盖全局配置:

1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName("address")
public class Address {
// 其他字段省略...

/**
* 逻辑删除
*/
@TableLogic(value = "0", delval = "1") // value=未删值,delval=已删值
private Boolean deleted;
}

3. 测试逻辑删除

调用普通 CRUD 方法,MP 会自动处理逻辑删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
public class LogicDeleteTest {
@Autowired
private IAddressService addressService;

@Test
void testRemove() {
// 1. 执行删除(实际执行UPDATE,而非DELETE)
addressService.removeById(59L);

// 2. 查询已删除数据(自动过滤,返回null)
Address address = addressService.getById(59L);
System.out.println("查询结果:" + address); // null
}
}

生成的 SQL

  • 删除:UPDATE address SET deleted=1 WHERE id=? AND deleted=0
  • 查询:SELECT * FROM address WHERE id=? AND deleted=0

4.3.2 注意事项

  • 仅 MP 自动生成的 SQL 支持逻辑删除,自定义 SQL 需手动添加 deleted 条件
  • 逻辑删除会导致表中 “垃圾数据” 累积,影响查询效率,建议定期将已删除数据迁移至历史表;
  • 不适用于需彻底清理数据的场景(如测试数据)。

4.4 枚举处理器:枚举与数据库字段自动转换

当 PO 类属性为枚举类型时,MyBatis 默认使用 EnumOrdinalTypeHandler(存储枚举索引),灵活性差。

MyBatis-Plus 提供 MybatisEnumTypeHandler,支持将枚举的指定属性(如 value)与数据库字段映射,且支持 JSON 序列化自定义。

4.4.1 实现步骤

1. 定义枚举并标记 @EnumValue

@EnumValue 注解标记枚举中与数据库字段对应的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

@Getter
public enum UserStatus {
NORMAL(1, "正常"), // 数据库存1,前端显示"正常"
FREEZE(2, "冻结"); // 数据库存2,前端显示"冻结"

@EnumValue // 标记数据库存储的属性
private final int value;

@JsonValue // 标记JSON序列化返回的属性(前端展示用)
private final String desc;

UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}

2. 配置全局枚举处理器

application.yaml 中指定默认枚举处理器:

1
2
3
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

3. 改造 PO 类

将 PO 类中的状态字段改为枚举类型:

1
2
3
4
5
6
7
8
9
10
@Data
@TableName("user")
public class User {
// 其他字段省略...

/**
* 用户状态(1正常,2冻结)
*/
private UserStatus status;
}

4.4.2 测试效果

1
2
3
4
5
6
7
8
9
10
@Test
void testEnumQuery() {
// 查询状态为正常的用户
List<User> users = userService.lambdaQuery()
.eq(User::getStatus, UserStatus.NORMAL)
.list();

// 输出结果:status字段为枚举对象,JSON返回desc属性
users.forEach(user -> System.out.println(user.getStatus().getDesc())); // 正常、正常...
}

生成的 SQL

1
SELECT * FROM user WHERE status = ?  -- 参数:1(UserStatus.NORMAL的value)

4.5 JSON 类型处理器:JSON 字段与对象自动转换

数据库中 JSON 类型字段(如 user.info)默认以字符串形式映射到 PO 类,读取其中属性需手动解析 JSON。MyBatis-Plus 提供 JacksonTypeHandler,可将 JSON 字段直接映射为 Java 对象(如 UserInfo),实现自动序列化 / 反序列化。

4.5.1 实现步骤

1. 定义 JSON 对应的实体类

创建与 JSON 字段结构匹配的实体类:

1
2
3
4
5
6
7
@Data
@AllArgsConstructor(staticName = "of") // 便捷构建对象
public class UserInfo {
private Integer age; // 对应JSON的age字段
private String intro; // 对应JSON的intro字段
private String gender; // 对应JSON的gender字段
}

2. 改造 PO 类

指定 JSON 字段的类型处理器,并开启自动 ResultMap 映射:

1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName(value = "user", autoResultMap = true) // 开启自动ResultMap(解决嵌套映射问题)
public class User {
// 其他字段省略...

/**
* 详细信息(JSON类型)
*/
@TableField(typeHandler = JacksonTypeHandler.class) // 指定JSON处理器
private UserInfo info;
}

3. 测试自动转换

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testJsonField() {
// 1. 新增用户(自动将UserInfo转为JSON字符串)
User user = new User();
user.setUsername("JsonTest");
user.setInfo(UserInfo.of(25, "技术宅", "male")); // 直接设置对象
userService.save(user);

// 2. 查询用户(自动将JSON字符串转为UserInfo)
User queryUser = userService.getById(user.getId());
System.out.println("用户年龄:" + queryUser.getInfo().getAge()); // 25
}

生成的 SQL

  • 新增:INSERT INTO user (username, info) VALUES (?, ?) → 参数:JsonTest{"age":25,"intro":"技术宅","gender":"male"}
  • 查询:自动将 JSON 字符串解析为 UserInfo 对象。

4.6 YAML 配置加密:保护敏感信息

配置文件中数据库用户名、密码等敏感信息以明文存储,存在泄露风险。MyBatis-Plus 基于 AES 算法提供配置加密功能,可对敏感信息加密存储,启动时解密。

4.6.1 实现步骤

1. 生成密钥与密文

使用 MP 提供的 AES 工具生成 16 位密钥,并加密用户名 / 密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;

public class ConfigEncryptTest {
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";

@Test
void generateEncryptInfo() {
// 1. 生成16位随机AES密钥
String key = AES.generateRandomKey();
System.out.println("密钥:" + key); // 例:7pSEa6F9TnYacTNJ

// 2. 加密用户名和密码
String encryptUsername = AES.encrypt(USERNAME, key);
String encryptPassword = AES.encrypt(PASSWORD, key);
System.out.println("加密后用户名:" + encryptUsername); // 例:O4Yq+WKYGlPW5t8QvgrhUQ==
System.out.println("加密后密码:" + encryptPassword); // 例:cDYHnWysq07zUIAy1tcbRQ==
}
}

2. 修改配置文件

将密文写入 application.yaml,密文需以 mpw: 为前缀:

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: mpw:O4Yq+WKYGlPW5t8QvgrhUQ== # 加密用户名
password: mpw:cDYHnWysq07zUIAy1tcbRQ== # 加密密码

3. 配置启动密钥

启动项目时传入密钥,支持两种方式:

  • Idea 启动:在 Run/Debug ConfigurationsProgram arguments 中添加 --mpw.key=7pSEa6F9TnYacTNJ

  • 单元测试:在测试类注解中添加密钥:

    1
    2
    3
    4
    @SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ")
    public class UserServiceTest {
    // 测试代码...
    }

随意运行一个单元测试,可以发现数据库查询正常。

4.6.2 实现原理

MP 通过重写 Spring 的 EnvironmentPostProcessor 接口,在项目启动前执行以下逻辑:

  1. 从启动参数中读取密钥 mpw.key
  2. 遍历配置文件中以 mpw: 为前缀的属性,用密钥解密;
  3. 将解密后的明文放入环境变量,覆盖密文配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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 {

@Override
public 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));
}
}
}
}

4.7 自动填充:字段值自动赋值

对于创建时间(create_time)、更新时间(update_time)等通用字段,每次新增 / 更新都需手动赋值。MyBatis-Plus 提供自动填充功能,通过配置处理器实现字段值的自动填充。

4.7.1 实现步骤

1. 实现填充处理器

创建类实现 MetaObjectHandler 接口,定义填充逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Slf4j
@Component // 交给Spring管理
public class MyMetaObjectHandler implements MetaObjectHandler {

// 新增时填充
@Override
public void insertFill(MetaObject metaObject) {
log.info("执行新增填充...");
// 填充创建时间(仅当字段为null时填充)
this.strictInsertFill(
metaObject,
"createTime", // PO类中的字段名
LocalDateTime.class,
LocalDateTime.now() // 填充值
);
// 填充更新时间(新增时也需赋值)
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}

// 更新时填充
@Override
public void updateFill(MetaObject metaObject) {
log.info("执行更新填充...");
// 填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}

2. 标记 PO 类字段

@TableField(fill) 注解标记需要自动填充的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@TableName("user")
public class User {
// 其他字段省略...

/**
* 创建时间(仅新增时填充)
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

/**
* 更新时间(新增和更新时填充)
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

3. 测试自动填充

1
2
3
4
5
6
7
8
9
10
11
@Test
void testAutoFill() {
User user = new User();
user.setUsername("AutoFillTest");
userService.save(user); // 无需手动设置createTime和updateTime

// 验证填充结果
User queryUser = userService.getById(user.getId());
System.out.println("创建时间:" + queryUser.getCreateTime()); // 自动赋值当前时间
System.out.println("更新时间:" + queryUser.getUpdateTime()); // 与创建时间一致
}

4.7.2 注意事项

  • 填充逻辑仅对 insert(T)updateById(T) 等传入实体对象的方法生效,update(Wrapper<T>) 需手动赋值;
  • strictInsertFill/strictUpdateFill 会严格匹配字段名和类型,避免误填充;
  • 若实体类字段已手动赋值,填充逻辑不会覆盖(默认策略)。

五、MyBatis-Plus 插件功能

MyBatis-Plus 基于 MyBatis 的拦截器机制,提供了一系列 可插拔插件,用于拓展核心功能(如分页、多租户、乐观锁等)。插件只需简单配置即可启用,且支持多插件组合使用(需注意配置顺序)。本章以最常用的 分页插件 为核心,详解插件的使用流程与实战技巧。

5.1 插件体系概述

MyBatis-Plus 内置多款实用插件,覆盖分页、数据安全、SQL 规范等高频场景,核心插件及功能如下:

插件类名 核心功能 适用场景
PaginationInnerInterceptor 自动分页(拦截查询 SQL,拼接分页语句) 列表查询分页需求
TenantLineInnerInterceptor 多租户(自动拼接租户 ID 条件) 多租户系统数据隔离
DynamicTableNameInnerInterceptor 动态表名(运行时切换表名) 分表场景(如按时间分表)
OptimisticLockerInnerInterceptor 乐观锁(基于版本号防止并发更新冲突) 高并发更新场景(如余额修改)
IllegalSQLInnerInterceptor SQL 性能规范(检测垃圾 SQL,如 SELECT *) 规范 SQL 编写,提升性能
BlockAttackInnerInterceptor 防止全表更新 / 删除(拦截无 WHERE 条件的 DML) 避免误操作导致全表数据变更

5.1.1 插件配置顺序

多插件组合使用时,需按以下顺序添加到拦截器,避免功能冲突:

  1. 多租户插件(TenantLineInnerInterceptor)→ 动态表名插件(DynamicTableNameInnerInterceptor
  2. 分页插件(PaginationInnerInterceptor)→ 乐观锁插件(OptimisticLockerInnerInterceptor
  3. SQL 性能规范插件(IllegalSQLInnerInterceptor)→ 防止全表操作插件(BlockAttackInnerInterceptor

5.2 核心插件:分页插件(PaginationInnerInterceptor)

MyBatis-Plus 本身不支持分页,需通过分页插件实现 —— 插件会自动拦截查询请求,在 SQL 后拼接分页语句(如 LIMIT),并封装总条数、总页数等分页信息。

5.2.1 步骤 1:引入依赖

⚠ 注意,MyBatis-Plus 于3.5.9 版本起,分页插件从核心包分离,需单独引入依赖,区分 JDK 版本:

JDK 11+ 依赖

1
2
3
4
5
6
<!-- MP分页插件(JDK 11+) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.9</version>
</dependency>

JDK 8+ 依赖

1
2
3
4
5
6
<!-- MP分页插件(JDK 8+) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>3.5.9</version>
</dependency>

5.2.2 步骤 2:配置分页拦截器

创建 MyBatis 配置类,初始化核心拦截器并添加分页插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;

@Configuration
public class MybatisConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1. 初始化核心拦截器(管理所有内置插件)
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 2. 配置分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 单页最大条数限制(避免一次查询过多数据)
interceptor.addInnerInterceptor(pageInterceptor);

// 如需添加其他插件,按顺序追加:interceptor.addInnerInterceptor(其他插件);
return interceptor;
}
}

5.2.3 步骤 3:使用分页 API

分页插件依赖 MP 提供的 Page 类封装分页参数,结合 BaseMapperIService 的分页方法实现查询。

基础分页示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.plugins.pagination.OrderItem;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class PaginationTest {

@Autowired
private IUserService userService;

@Test
void testBasicPage() {
// 1. 分页参数:第1页,每页2条数据
int pageNo = 1;
int pageSize = 2;
Page<User> page = Page.of(pageNo, pageSize);

// 2. 排序参数:按余额降序(new OrderItem() 为旧写法,推荐静态方法)
page.addOrder(OrderItem.desc("balance")); // 降序
page.addOrder(OrderItem.asc("id")); // 再按ID升序

// 3. 执行分页查询(IService.page() 方法)
Page<User> resultPage = userService.page(page);

// 4. 解析分页结果
long total = resultPage.getTotal(); // 总条数
long pages = resultPage.getPages(); // 总页数
List<User> records = resultPage.getRecords(); // 当前页数据

System.out.println("total = " + total);
System.out.println("pages = " + pages);
records.forEach(System.out::println);
}
}

分页查询结果

生成的 SQL

插件会自动拼接分页语句,同时查询总条数:

1
2
3
4
-- 查询当前页数据
SELECT id,username,balance,... FROM user ORDER BY balance DESC, id ASC LIMIT ?, ?
-- 查询总条数
SELECT COUNT(*) FROM user

5.3 实战:通用分页实体设计与优化

实际开发中,分页查询需适配多业务场景(如动态条件、排序、VO 转换),直接使用 Page 类会导致代码重复。通过设计 通用分页实体,可封装分页参数转换、结果封装等逻辑,提升开发效率。

5.3.1 核心实体类设计

需定义 4 个实体类,分别对应 “分页参数接收”“业务条件接收”“分页结果返回”“视图对象”:

1. 通用分页参数(PageDTO)

接收前端传入的分页与排序参数,适用于所有分页场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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("页码(默认1)")
private Integer pageNo = 1;

@ApiModelProperty("每页条数(默认10)")
private Integer pageSize = 10;

@ApiModelProperty("排序字段(如balance、update_time)")
private String sortBy;

@ApiModelProperty("是否升序(true=升序,false=降序)")
private Boolean isAsc = true;

/**
* 转换为MP的Page对象(带排序)
* @param <T> 泛型(PO类型)
* @return Page对象
*/
public <T> Page<T> toMpPage() {
Page<T> page = Page.of(pageNo, pageSize);
// 有排序字段则按传入规则排序,否则默认按update_time降序
if (sortBy != null && !sortBy.trim().isEmpty()) {
page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));
} else {
page.addOrder(OrderItem.desc("update_time"));
}
return page;
}

/**
* 自定义默认排序(覆盖默认的update_time降序)
*/
public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {
Page<T> page = Page.of(pageNo, pageSize);
if (sortBy != null && !sortBy.trim().isEmpty()) {
page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));
} else {
page.addOrder(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));
}
return page;
}
}

2. 业务查询条件(UserQueryDTO)

继承 PageDTO,添加用户查询的业务条件(如用户名、状态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.mp.domain.po.User;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
@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;

/**
* 转换为Lambda查询条件(封装业务筛选逻辑)
*/
public LambdaQueryWrapper<User> toQueryWrapper() {
return new LambdaQueryWrapper<User>()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance);
}
}

3. 分页结果封装(PageResult)

统一分页结果格式,包含总条数、总页数、当前页数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 = "分页结果")
public class PageResult<T> {
@ApiModelProperty("总条数")
private Long total;

@ApiModelProperty("总页数")
private Long pages;

@ApiModelProperty("当前页数据")
private List<T> list;

/**
* PO分页结果转换为VO分页结果(默认属性拷贝)
* @param page MP分页结果(PO)
* @param voClazz VO类字节码
* @param <PO> PO类型
* @param <VO> VO类型
* @return 分页结果(VO)
*/
public static <PO, VO> PageResult<VO> of(Page<PO> page, Class<VO> voClazz) {
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 属性拷贝
result.setList(BeanUtil.copyToList(records, voClazz));
return result;
}

/**
* 自定义PO→VO转换(支持特殊逻辑,如脱敏、字段处理)
* @param page MP分页结果(PO)
* @param converter 转换函数
* @param <PO> PO类型
* @param <VO> VO类型
* @return 分页结果(VO)
*/
public static <PO, VO> PageResult<VO> of(Page<PO> page, Function<PO, VO> converter) {
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;
}

// 自定义转换逻辑
result.setList(records.stream().map(converter).collect(Collectors.toList()));
return result;
}
}

4. 视图对象(UserVO)

返回给前端的用户数据(隐藏敏感字段,如密码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.itheima.mp.domain.po.UserInfo;
import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户视图对象")
public class UserVO {
@ApiModelProperty("用户ID")
private Long id;

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("详细信息")
private UserInfo info;

@ApiModelProperty("用户状态")
private UserStatus status;

@ApiModelProperty("账户余额")
private Integer balance;
}

5.3.2 分页接口开发

基于通用实体实现 “用户条件分页查询” 接口,代码简洁且可复用。

1. Controller 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.itheima.mp.domain.dto.UserQueryDTO;
import com.itheima.mp.domain.vo.PageResult;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
@Api(tags = "用户管理接口")
public class UserController {

@Autowired
private IUserService userService;

@GetMapping("/page")
@ApiOperation("用户条件分页查询")
public PageResult<UserVO> queryUserByPage(UserQueryDTO queryDTO) {
return userService.queryUserByPage(queryDTO);
}
}

2. Service 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.mp.domain.dto.UserQueryDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.PageResult;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

@Override
public PageResult<UserVO> queryUserByPage(UserQueryDTO queryDTO) {
// 1. 转换分页参数(PageDTO → MP Page)
Page<User> page = queryDTO.toMpPage();

// 2. 转换查询条件(UserQueryDTO → LambdaQueryWrapper)
Page<User> resultPage = lambdaQuery()
.like(queryDTO.getName() != null, User::getUsername, queryDTO.getName())
.eq(queryDTO.getStatus() != null, User::getStatus, queryDTO.getStatus())
.ge(queryDTO.getMinBalance() != null, User::getBalance, queryDTO.getMinBalance())
.le(queryDTO.getMaxBalance() != null, User::getBalance, queryDTO.getMaxBalance())
.page(page);

// 3. 转换分页结果(PO Page → VO PageResult)
// 普通转换(属性拷贝)
// return PageResult.of(resultPage, UserVO.class);

// 自定义转换(示例:用户名脱敏)
return PageResult.of(resultPage, user -> {
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 脱敏逻辑:用户名保留前2位,其余用*代替
String username = vo.getUsername();
if (username.length() > 2) {
vo.setUsername(username.substring(0, 2) + "*".repeat(username.length() - 2));
}
return vo;
});
}
}

5.3.3 接口测试与结果

请求参数(GET)

1
/users/page?pageNo=1&pageSize=2&sortBy=balance&isAsc=false&name=o&status=1

响应结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"total": 3,
"pages": 2,
"list": [
{
"id": 1,
"username": "Ja**",
"info": {"age":20,"intro":"佛系青年","gender":"male"},
"status": "NORMAL",
"balance": 1600
},
{
"id": 2,
"username": "Ro**",
"info": {"age":19,"intro":"青涩少女","gender":"female"},
"status": "NORMAL",
"balance": 600
}
]
}

5.4 插件功能总结

  1. 分页插件核心价值:自动拼接分页 SQL,封装分页元数据,避免手动编写 LIMIT 和 count 查询,适配所有单表查询场景。
  2. 通用分页实体设计:通过 PageDTO 封装分页参数转换、PageResult 封装结果,减少重复代码,支持自定义转换逻辑(如脱敏、字段处理)。
  3. 多插件组合要点:严格按 “租户 / 动态表名→分页 / 乐观锁→SQL 规范 / 防全表操作” 的顺序配置,避免拦截逻辑冲突。

MyBatis-Plus 插件体系以 “轻量级、可插拔” 为设计理念,既能满足复杂业务需求,又不侵入原有代码,是提升开发效率与系统健壮性的重要工具。