
一、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字段作为主键,减少手动配置成本。
- 灵活条件构造:提供
QueryWrapper、LambdaQueryWrapper 等条件构造器,支持动态拼接复杂 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 个核心层级:
- 底层依赖层:依赖 MyBatis 核心包、JDBC 驱动及 Spring 生态组件(如 Spring Boot Starter),确保与 Java 主流开发环境无缝集成。
- 核心功能层:MP 的 “心脏”,包含三大核心能力:
- 通用 CRUD 接口:
BaseMapper(提供单表 DAO 层操作)、IService(提供 Service 层批量操作、逻辑封装);
- 实体映射:基于注解(
@TableName、@TableId 等)实现表与实体类的关联;
- 条件构造:
Wrapper 家族接口,动态生成 WHERE 条件。
- 扩展功能层:针对高频需求的封装,如代码生成器(自动生成 Entity、Mapper、Service 等代码)、逻辑删除(模拟删除效果,不真正删数据)、枚举 / JSON 处理器(解决特殊字段类型映射问题)。
- 插件层:基于 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 |
用户地址表 |
id、user_id(外键关联 user.id)、province(省)、city(市)、is_default(默认地址) |
tb_user |
注解测试专用表 |
user_id(主键)、username、is_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 项目导入与配置
- 导入项目:加载
mp-demo项目结构,确保核心包路径为com.itheima.mp(含domain.po、mapper等子包)。
- 配置数据库连接:修改
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 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
| <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") @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
| 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 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;
public interface UserMapper extends BaseMapper<User> { }
|
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); }
@Test void testSelectById() { User user = userMapper.selectById(5L); System.out.println("user = " + user); }
@Test void testSelectByIds() { List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L)); users.forEach(System.out::println); }
@Test void testUpdateById() { User user = new User(); user.setId(5L); user.setBalance(20000); userMapper.updateById(user); }
@Test void testDelete() { userMapper.deleteById(5L); } }
|
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") public class MpUser { @TableId(value = "user_id", type = IdType.AUTO) private Long id;
@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; }
|
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"); mpUser.setOrder(1); mpUser.setAddress("测试地址"); mpUserMapper.insert(mpUser); }
@Test void testSelectById() { MpUser mpUser = mpUserMapper.selectById(1L); System.out.println(mpUser); } }
|
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: type-aliases-package: com.itheima.mp.domain.po mapper-locations: "classpath*:/mapper/**/*.xml" configuration: map-underscore-to-camel-case: true cache-enabled: false global-config: db-config: id-type: assign_id update-strategy: not_null table-prefix: tb_
|
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 是条件构造的核心抽象类,其设计采用了分层继承模式,不同子类对应不同场景的条件构造需求。
继承关系

核心类作用
-
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() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("id", "username", "info", "balance") .like("username", "o") .ge("balance", 1000); 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 >= ?
|
2. 基于条件的更新
需求:将用户名为 “Jack” 的用户余额更新为 2000 元。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Test void testQueryWrapperUpdate() { User user = new User(); user.setBalance(2000); QueryWrapper<User> queryWrapper = new QueryWrapper<User>() .eq("username", "Jack"); int rows = userMapper.update(user, queryWrapper); System.out.println("更新成功:" + (rows > 0)); }
|
生成的 SQL:
1 2 3 4
| UPDATE user SET balance=? WHERE username = ?
|
3. 基于条件的删除
需求:删除状态为 “冻结”(status=2)且余额为 0 的用户。
1 2 3 4 5 6 7 8 9 10 11
| @Test void testQueryWrapperDelete() { QueryWrapper<User> queryWrapper = new QueryWrapper<>() .eq("status", 2) .eq("balance", 0); int rows = userMapper.delete(queryWrapper); System.out.println("删除成功:" + (rows > 0)); }
|
生成的 SQL:
1 2 3
| DELETE FROM user WHERE status = ? AND balance = ?
|
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() { List<Long> ids = List.of(1L, 2L, 4L); UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>() .setSql("balance = balance - 200") .in("id", ids); 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 (?, ?, ?)
|
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>() .setSql("balance = balance + 500") .set("status", 1) .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 = ?
|
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() { LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(User::getStatus, 1) .like(User::getUsername, "o"); 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<>(); lambdaUpdateWrapper.setSql("balance = balance * 2") .set(User::getUpdateTime, LocalDateTime.now()) .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 = ?
|
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 条件构造器使用总结
- 场景选择:
- 简单查询 / 更新 / 删除:优先使用
LambdaQueryWrapper(避免硬编码)。
- 复杂更新(如基于字段计算):使用
LambdaUpdateWrapper。
- 特殊场景(如多表联查需手动写 SQL):可混合使用 Wrapper 和 XML 映射。
- 最佳实践:
- 始终优先使用 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 核心逻辑由自定义实现”。
实现步骤
- Service 层构建条件:用 Wrapper 定义查询条件(避免业务层写 SQL);
- Mapper 层声明方法:用
@Param("ew") 接收 Wrapper(参数名固定为ew,可用Constants.WRAPPER常量替代);
- 编写自定义 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) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>() .in(User::getId, ids); baseMapper.updateBalanceByIds(wrapper, amount); } }
|
2. Mapper 层:自定义 SQL 片段
支持 注解式 SQL 或 XML 式 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> { @Update("UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}") void updateBalanceByIds( @Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper, // 固定参数名ew @Param("amount") int amount );
}
|
3. 生成的 SQL
1 2
| UPDATE user SET balance = balance - 200 WHERE id IN (?, ?, ?)
|
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) { QueryWrapper<User> wrapper = new QueryWrapper<User>() .in("u.id", userIds) .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("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 = ?
|
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;
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 {
@Override public void deductBalanceById(Long id, Integer money) { User user = getById(id); if (user == null || user.getStatus() == 2) { throw new RuntimeException("用户状态异常"); } if (user.getBalance() < money) { throw new RuntimeException("余额不足"); } baseMapper.deductBalance(id, money); } }
|
步骤 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;
@PostMapping public void save(@RequestBody UserFormDTO dto) { User user = BeanUtil.copyProperties(dto, User.class); userService.save(user); }
@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) { 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(); }
|
场景 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) { User user = getById(id); if (user == null || user.getStatus() == 2) { throw new RuntimeException("用户异常"); } int remain = user.getBalance() - money; if (remain < 0) { throw new RuntimeException("余额不足"); } lambdaUpdate() .set(User::getBalance, remain) .set(remain == 0, User::getStatus, 2) .eq(User::getId, id) .update(); }
|
5. 性能优化:批量新增与批处理
IService 的 saveBatch() 方法默认采用 “预编译批处理”,但需配合 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 使用总结
- 核心价值:封装 Service 层模板方法,减少重复代码(如批量操作、条件查询);
- 最佳实践:
- 自定义 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 表的基础代码为例:
-
配置数据库连接:顶部菜单选择 Other → Config Database,填写数据库 URL、用户名、密码,测试连接通过后保存。

-
配置生成参数:再次选择 Other → Code Generator,填写核心参数:
package:父包路径(如 com.itheima.mp);
Entity:实体类包路径(如 domain.po);
TablePrefix:表前缀(如无则留空);
Id策略:与数据库主键策略一致(如 AUTO 自增);
- 勾选需生成的组件(Entity、Mapper、Service、Controller 等)。


-
生成代码:点击 “提交”,插件自动在指定包路径下生成代码,包含 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
| <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"); String outputDir = System.getProperty("user.dir") + "/src/main/java";
FastAutoGenerator.create(JDBC_URL, JDBC_USER, JDBC_PWD) .globalConfig(builder -> { builder.author("Aizen") .outputDir(outputDir) .enableSwagger() .commentDate("yyyy-MM-dd"); }) .packageConfig(builder -> { builder.parent("com.itheima.mp") .entity("domain.po") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") .xml("src/main/resources/mapper"); }) .strategyConfig(builder -> { builder.addInclude(tables) .addTablePrefix() .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .serviceBuilder() .formatServiceFileName("%sService") .formatServiceImplFileName("%sServiceImpl") .controllerBuilder() .enableRestStyle() .mapperBuilder() .superClass(BaseMapper.class) .enableBaseResultMap(); }) .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 {
@Test void testDbGetById() { User user = Db.getById(1L, User.class); System.out.println("用户:" + user); }
@Test void testDbLambdaQuery() { List<User> userList = Db.lambdaQuery(User.class) .like(User::getUsername, "o") .ge(User::getBalance, 1000) .list(); userList.forEach(System.out::println); }
@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) { User user = getById(id); if (user == null || user.getStatus() == 2) { throw new RuntimeException("用户不存在或冻结"); }
List<Address> addresses = Db.lambdaQuery(Address.class) .eq(Address::getUserId, id) .list();
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) { List<User> users = listByIds(ids); if (users.isEmpty()) return Collections.emptyList();
List<Long> userIds = users.stream().map(User::getId).toList(); List<Address> addresses = Db.lambdaQuery(Address.class) .in(Address::getUserId, userIds) .list();
Map<Long, List<AddressVO>> addressMap = addresses.stream() .map(addr -> BeanUtil.copyProperties(addr, AddressVO.class)) .collect(Collectors.groupingBy(AddressVO::getUserId));
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 logic-delete-value: 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") 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() { addressService.removeById(59L);
Address address = addressService.getById(59L); System.out.println("查询结果:" + address); } }
|
生成的 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, "正常"), FREEZE(2, "冻结");
@EnumValue private final int value;
@JsonValue 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 {
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(); users.forEach(user -> System.out.println(user.getStatus().getDesc())); }
|
生成的 SQL:
1
| SELECT * FROM user WHERE status = ?
|
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; private String intro; private String gender; }
|
2. 改造 PO 类
指定 JSON 字段的类型处理器,并开启自动 ResultMap 映射:
1 2 3 4 5 6 7 8 9 10 11
| @Data @TableName(value = "user", autoResultMap = true) public class User {
@TableField(typeHandler = JacksonTypeHandler.class) private UserInfo info; }
|
3. 测试自动转换
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testJsonField() { User user = new User(); user.setUsername("JsonTest"); user.setInfo(UserInfo.of(25, "技术宅", "male")); userService.save(user);
User queryUser = userService.getById(user.getId()); System.out.println("用户年龄:" + queryUser.getInfo().getAge()); }
|
生成的 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() { String key = AES.generateRandomKey(); System.out.println("密钥:" + key);
String encryptUsername = AES.encrypt(USERNAME, key); String encryptPassword = AES.encrypt(PASSWORD, key); System.out.println("加密后用户名:" + encryptUsername); System.out.println("加密后密码:" + encryptPassword); } }
|
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 Configurations 的 Program arguments 中添加 --mpw.key=7pSEa6F9TnYacTNJ;

-
单元测试:在测试类注解中添加密钥:
1 2 3 4
| @SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ") public class UserServiceTest { }
|
随意运行一个单元测试,可以发现数据库查询正常。
4.6.2 实现原理
MP 通过重写 Spring 的 EnvironmentPostProcessor 接口,在项目启动前执行以下逻辑:
- 从启动参数中读取密钥
mpw.key;
- 遍历配置文件中以
mpw: 为前缀的属性,用密钥解密;
- 将解密后的明文放入环境变量,覆盖密文配置。
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;
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 public class MyMetaObjectHandler implements MetaObjectHandler {
@Override public void insertFill(MetaObject metaObject) { log.info("执行新增填充..."); this.strictInsertFill( metaObject, "createTime", 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);
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 插件配置顺序
多插件组合使用时,需按以下顺序添加到拦截器,避免功能冲突:
- 多租户插件(
TenantLineInnerInterceptor)→ 动态表名插件(DynamicTableNameInnerInterceptor)
- 分页插件(
PaginationInnerInterceptor)→ 乐观锁插件(OptimisticLockerInnerInterceptor)
- SQL 性能规范插件(
IllegalSQLInnerInterceptor)→ 防止全表操作插件(BlockAttackInnerInterceptor)
MyBatis-Plus 本身不支持分页,需通过分页插件实现 —— 插件会自动拦截查询请求,在 SQL 后拼接分页语句(如 LIMIT),并封装总条数、总页数等分页信息。
5.2.1 步骤 1:引入依赖
⚠ 注意,MyBatis-Plus 于3.5.9 版本起,分页插件从核心包分离,需单独引入依赖,区分 JDK 版本:
JDK 11+ 依赖
1 2 3 4 5 6
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.9</version> </dependency>
|
JDK 8+ 依赖
1 2 3 4 5 6
| <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() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); pageInterceptor.setMaxLimit(1000L); interceptor.addInnerInterceptor(pageInterceptor); return interceptor; } }
|
5.2.3 步骤 3:使用分页 API
分页插件依赖 MP 提供的 Page 类封装分页参数,结合 BaseMapper 或 IService 的分页方法实现查询。
基础分页示例
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() { int pageNo = 1; int pageSize = 2; Page<User> page = Page.of(pageNo, pageSize);
page.addOrder(OrderItem.desc("balance")); page.addOrder(OrderItem.asc("id"));
Page<User> resultPage = userService.page(page);
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;
public <T> Page<T> toMpPage() { Page<T> page = Page.of(pageNo, pageSize); if (sortBy != null && !sortBy.trim().isEmpty()) { page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc)); } else { page.addOrder(OrderItem.desc("update_time")); } return page; }
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;
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); } }
|
统一分页结果格式,包含总条数、总页数、当前页数据:
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;
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; }
result.setList(BeanUtil.copyToList(records, voClazz)); return result; }
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) { Page<User> page = queryDTO.toMpPage();
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);
return PageResult.of(resultPage, user -> { UserVO vo = BeanUtil.copyProperties(user, UserVO.class); 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 插件功能总结
- 分页插件核心价值:自动拼接分页 SQL,封装分页元数据,避免手动编写
LIMIT 和 count 查询,适配所有单表查询场景。
- 通用分页实体设计:通过
PageDTO 封装分页参数转换、PageResult 封装结果,减少重复代码,支持自定义转换逻辑(如脱敏、字段处理)。
- 多插件组合要点:严格按 “租户 / 动态表名→分页 / 乐观锁→SQL 规范 / 防全表操作” 的顺序配置,避免拦截逻辑冲突。
MyBatis-Plus 插件体系以 “轻量级、可插拔” 为设计理念,既能满足复杂业务需求,又不侵入原有代码,是提升开发效率与系统健壮性的重要工具。
