MyBatis-Plus详解
MyBatis-Plus 详解:从 BaseMapper 到企业项目实战
课程位置:第三阶段 Java 企业项目 + AI 助手
本阶段最后一个知识点:MyBatis-Plus
前置知识:Java、Spring Boot、MyBatis、MySQL、Maven、Lambda、Stream、事务、企业开发流程
学习目标:从零理解 MyBatis-Plus 的定位,掌握 BaseMapper、IService、ServiceImpl、Wrapper、LambdaQueryWrapper、分页、自动填充、逻辑删除、乐观锁、主键策略、代码生成器,以及 MyBatis-Plus 与原生 MyBatis、若依项目的正确配合方式。
一、先说结论:MyBatis-Plus 是什么
MyBatis-Plus,简称:
MP
它不是:
MyBatis 的替代品
而是:
MyBatis 的增强工具
你可以理解成:
MyBatis
+
常用 CRUD 自动实现
+
条件构造器
+
分页
+
逻辑删除
+
乐观锁
+
自动填充
+
代码生成
二、为什么已经会 MyBatis 还要学 MyBatis-Plus
使用原生 MyBatis 时,一个简单查询可能需要:
Controller
↓
Service
↓
Mapper Interface
↓
Mapper XML
↓
SQL
例如:
SELECT *
FROM user
WHERE id = #{id};
这类简单 CRUD:
每个项目都重复写
MyBatis-Plus 的目标就是:
把重复的单表 CRUD 省掉
三、MyBatis-Plus 最适合什么
非常适合:
单表新增
单表删除
单表修改
根据主键查询
简单条件查询
分页查询
批量保存
简单统计
四、MyBatis-Plus 不代表不用 SQL
遇到:
复杂 JOIN
复杂统计
报表
窗口函数
子查询
复杂数据权限
复杂动态 SQL
仍然可以:
写 Mapper XML
五、最正确的理解
不是:
MyBatis 和 MyBatis-Plus 二选一
而是:
简单 CRUD
→ MyBatis-Plus
复杂 SQL
→ 原生 MyBatis XML
两者:
一起使用
六、企业项目为什么喜欢这种方式
因为:
能省代码
但又不会:
失去 SQL 控制能力
七、当前版本环境提醒
截至 2026 年课程整理时,
MyBatis-Plus 官方文档给出的当前稳定版本基线为:
3.5.17
Spring Boot 3 使用:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.17</version>
</dependency>
八、Spring Boot 2 和 3 Starter 不一样
Spring Boot 2:
mybatis-plus-boot-starter
Spring Boot 3:
mybatis-plus-spring-boot3-starter
九、为什么你要特别注意
你当前学习路线主要使用:
JDK17
Spring Boot 3
所以不要机械复制旧教程:
mybatis-plus-boot-starter
应该先确认:
当前 Spring Boot 主版本
十、不要同时乱加多个 MyBatis Starter
例如同时添加:
mybatis-spring-boot-starter
mybatis-plus-spring-boot3-starter
如果没有明确原因:
不建议
因为 MP Starter 本身已经:
整合 MyBatis
十一、最小依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.17</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
十二、分页特别提醒
从 MyBatis-Plus:
3.5.9+
开始,
分页插件使用的 JSQLParser:
被拆分为可选依赖
因此使用分页插件时,需要根据版本额外引入对应模块。
JDK11+ 常见:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
十三、为什么这个特别容易踩坑
旧教程通常只写:
PaginationInnerInterceptor
你照着复制后:
类可能找不到
或分页插件相关依赖缺失
所以必须:
看当前官方文档
十四、建立第一个练习项目
表:
CREATE TABLE sys_student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
age INT,
gender TINYINT,
phone VARCHAR(20),
create_time DATETIME,
update_time DATETIME
);
十五、实体类
@Data
@TableName("sys_student")
public class Student {
@TableId(
value = "id",
type = IdType.AUTO
)
private Long id;
private String name;
private Integer age;
private Integer gender;
private String phone;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
十六、@TableName
作用:
指定实体类对应哪张数据库表
例如:
@TableName("sys_student")
十七、什么时候可以不写 @TableName
如果类名:
Student
表名:
student
并且命名转换可以正确匹配,
可以不写。
十八、为什么企业项目仍常写
因为:
更明确
不依赖猜测
重构类名不容易误伤表名
十九、@TableId
作用:
标识主键
二十、IdType.AUTO
表示:
数据库自增
适合:
MySQL AUTO_INCREMENT
二十一、IdType.ASSIGN_ID
MyBatis-Plus:
自动分配 Long 类型 ID
常用于:
分布式唯一 ID
二十二、AUTO 和 ASSIGN_ID 怎么选
单体项目:
AUTO
简单直观。
分布式:
ASSIGN_ID
可以避免多个数据库节点:
自增主键冲突
二十三、不要把业务编号当主键
例如:
ST202609100001
最好作为:
student_no / order_no / settlement_no
数据库关联仍用:
BIGINT id
二十四、Mapper 是最核心的第一步
@Mapper
public interface StudentMapper
extends BaseMapper<Student> {
}
二十五、BaseMapper 是什么
MyBatis-Plus 提供:
BaseMapper<T>
里面已经提供:
insert
deleteById
updateById
selectById
selectList
selectCount
selectPage
等常用方法。
二十六、为什么只 extends 就能用
项目启动时:
MyBatis-Plus 根据实体元数据
自动注入常用 CRUD SQL
所以:
不需要自己写最基础 XML
二十七、第一个新增
Student student =
new Student();
student.setName("张三");
student.setAge(20);
student.setGender(1);
studentMapper.insert(
student
);
二十八、执行后
如果:
id 自增
通常实体:
student.getId()
会得到:
数据库生成 ID
二十九、根据 ID 查询
Student student =
studentMapper
.selectById(1L);
三十、根据 ID 修改
先:
Student student =
new Student();
student.setId(1L);
student.setName("李四");
再:
studentMapper
.updateById(student);
三十一、updateById 会不会更新所有字段
通常会根据:
字段更新策略
决定。
默认常见行为:
null 字段不参与更新
三十二、为什么这一点重要
如果你想:
把某字段明确更新成 NULL
不能想当然。
需要检查:
@TableField updateStrategy
或:
使用 UpdateWrapper.set(...)
三十三、删除
studentMapper
.deleteById(1L);
三十四、批量 ID 查询
List<Student> list =
studentMapper.selectByIds(
List.of(
1L,
2L,
3L
)
);
不同版本中批量 API:
可能存在历史方法命名变化
实际开发:
IDEA 查看 BaseMapper 当前方法
三十五、为什么要学会 Ctrl+B
在 IDEA:
Ctrl + B
跳转定义。
对:
BaseMapper
IService
Wrapper
特别有用。
三十六、Alt+7
可以查看:
类的方法结构
方便快速找:
BaseMapper 有什么方法
三十七、BaseMapper 不只是 CRUD
现代 MyBatis-Plus 还在持续增强:
批量
流式查询
Wrapper 更新
等能力。
但学习时:
先掌握基础 CRUD
三十八、selectList
无条件:
List<Student> list =
studentMapper.selectList(
null
);
三十九、为什么传 null
表示:
没有 Wrapper 条件
也就是:
SELECT ...
FROM sys_student;
四十、实际项目慎用无条件查询
如果表有:
1000 万数据
直接:
selectList(null)
非常危险。
四十一、记住
有分页需求
就分页
有条件
就带条件
四十二、Wrapper 是 MyBatis-Plus 第二核心
Wrapper:
条件构造器
作用:
用 Java 写 SQL WHERE 条件
四十三、例如原 SQL
SELECT *
FROM sys_student
WHERE
age >= 18
AND name LIKE '%张%';
Wrapper:
QueryWrapper<Student> wrapper =
new QueryWrapper<>();
wrapper.ge(
"age",
18
);
wrapper.like(
"name",
"张"
);
List<Student> list =
studentMapper
.selectList(wrapper);
四十四、QueryWrapper 的缺点
这里:
"age"
"name"
都是:
字符串字段名
如果实体字段改名:
编译器不会报错
四十五、所以更推荐 LambdaQueryWrapper
LambdaQueryWrapper<Student>
wrapper =
new LambdaQueryWrapper<>();
四十六、条件
wrapper
.ge(
Student::getAge,
18
)
.like(
Student::getName,
"张"
);
四十七、优势
类型安全
IDEA 可重构
减少字段拼写错误
四十八、所以企业开发推荐
简单实体条件:
LambdaQueryWrapper
四十九、创建 LambdaQueryWrapper 的几种方式
方式一:
new LambdaQueryWrapper<Student>()
五十、方式二
Wrappers
.lambdaQuery(
Student.class
);
五十一、方式三
如果 Service 支持:
lambdaQuery()
也可以:
studentService
.lambdaQuery()
五十二、eq
等于:
wrapper.eq(
Student::getName,
"张三"
);
对应:
name = '张三'
五十三、ne
不等于:
wrapper.ne(
Student::getGender,
0
);
五十四、gt
大于:
wrapper.gt(
Student::getAge,
18
);
五十五、ge
大于等于:
wrapper.ge(
Student::getAge,
18
);
五十六、lt
小于:
wrapper.lt(
Student::getAge,
60
);
五十七、le
小于等于。
五十八、between
wrapper.between(
Student::getAge,
18,
30
);
五十九、like
wrapper.like(
Student::getName,
"张"
);
六十、likeLeft
可以理解:
左边加 %
六十一、likeRight
可以理解:
右边加 %
六十二、in
wrapper.in(
Student::getId,
List.of(
1L,
2L,
3L
)
);
六十三、isNull
wrapper.isNull(
Student::getPhone
);
六十四、isNotNull
wrapper.isNotNull(
Student::getPhone
);
六十五、orderByAsc
wrapper.orderByAsc(
Student::getAge
);
六十六、orderByDesc
wrapper.orderByDesc(
Student::getCreateTime
);
六十七、select 指定字段
wrapper.select(
Student::getId,
Student::getName,
Student::getAge
);
六十八、为什么不要总 SELECT *
接口列表只需要:
id
name
age
就没必要:
把所有大字段都查出来
六十九、条件参数是 Wrapper 最实用的功能之一
例如:
wrapper.eq(
StringUtils.hasText(
query.getName()
),
Student::getName,
query.getName()
);
七十、第一个参数什么意思
true
→ 添加条件
false
→ 不添加条件
七十一、这比大量 if 更简洁
传统:
if (
StringUtils.hasText(
query.getName()
)
) {
wrapper.eq(
Student::getName,
query.getName()
);
}
MP:
wrapper.eq(
StringUtils.hasText(
query.getName()
),
Student::getName,
query.getName()
);
七十二、动态查询示例
LambdaQueryWrapper<Student>
wrapper =
Wrappers.lambdaQuery(
Student.class
);
wrapper
.like(
StringUtils.hasText(
query.getName()
),
Student::getName,
query.getName()
)
.ge(
query.getMinAge()
!= null,
Student::getAge,
query.getMinAge()
)
.le(
query.getMaxAge()
!= null,
Student::getAge,
query.getMaxAge()
)
.eq(
query.getGender()
!= null,
Student::getGender,
query.getGender()
)
.orderByDesc(
Student::getCreateTime
);
七十三、这是 MyBatis-Plus 非常核心的写法
面试:
经常问
七十四、and
需要括号:
WHERE
status = 1
AND (
name LIKE '%张%'
OR phone LIKE '%138%'
)
可以:
wrapper
.eq(
Student::getStatus,
1
)
.and(
w ->
w.like(
Student::getName,
"张"
)
.or()
.like(
Student::getPhone,
"138"
)
);
七十五、or
wrapper
.eq(
Student::getGender,
1
)
.or()
.eq(
Student::getGender,
2
);
七十六、为什么括号非常重要
下面两条 SQL:
A AND (B OR C)
和:
(A AND B) OR C
完全不同。
七十七、Wrapper 写复杂条件时
建议:
先写目标 SQL
再转:
Wrapper
七十八、不要直接凭感觉链式写
否则:
AND / OR 优先级容易错
七十九、QueryWrapper 什么时候仍然有价值
例如:
返回 Map
数据库字段表达式
某些动态字段
QueryWrapper:
更直接
八十、但普通实体查询优先 Lambda
八十一、Wrapper 安全问题
非常重要:
不要把 Wrapper 直接作为 Controller 请求参数
八十二、错误设计
@PostMapping("/list")
public List<User> list(
@RequestBody
QueryWrapper<User> wrapper
) {
return mapper.selectList(
wrapper
);
}
八十三、为什么危险
这样相当于:
让前端直接影响 SQL 条件结构
容易造成:
越权
非法 SQL
不可控查询
八十四、正确
前端传:
DTO
例如:
public class StudentQueryDTO {
private String name;
private Integer minAge;
private Integer maxAge;
private Integer gender;
}
八十五、后端自己构造 Wrapper
LambdaQueryWrapper<Student>
wrapper =
buildWrapper(query);
八十六、这也是企业开发安全边界
请求 DTO
≠
SQL Wrapper
八十七、last() 要慎用
例如:
wrapper.last(
"LIMIT 1"
);
八十八、为什么
last():
会直接拼到 SQL 尾部
绝不能:
把前端原始字符串传进去
八十九、危险
wrapper.last(
request.getSql()
);
绝对不要。
九十、apply / inSql / exists 等高级方法
功能强,
但:
使用原始 SQL 片段时
必须警惕注入风险
九十一、原则
能用普通 Wrapper API
就优先普通 API
九十二、UpdateWrapper
用于:
条件更新
九十三、LambdaUpdateWrapper
推荐:
LambdaUpdateWrapper<Student>
wrapper =
new LambdaUpdateWrapper<>();
九十四、例如
wrapper
.eq(
Student::getId,
1L
)
.set(
Student::getName,
"王五"
);
studentMapper.update(
wrapper
);
实际方法签名:
以当前 BaseMapper 为准
九十五、传统常见写法
studentMapper.update(
null,
wrapper
);
不同版本:
可能提供更便捷重载
九十六、UpdateWrapper 一个重要用途
把字段:
明确更新为 null
例如:
wrapper.set(
Student::getPhone,
null
);
九十七、另一个重要用途:带旧状态更新
企业业务:
非常实用
九十八、例如订单提交
LambdaUpdateWrapper<Order>
wrapper =
Wrappers.lambdaUpdate(
Order.class
);
wrapper
.eq(
Order::getId,
orderId
)
.eq(
Order::getStatus,
OrderStatus.DRAFT
)
.set(
Order::getStatus,
OrderStatus.SUBMITTED
);
int rows =
orderMapper.update(
wrapper
);
九十九、为什么这是好写法
生成逻辑类似:
UPDATE orders
SET status = 'SUBMITTED'
WHERE
id = ?
AND status = 'DRAFT';
一百、rows == 0
说明:
记录不存在
或状态已经变化
一百零一、这就是前面淘车湾项目里的并发思想
MyBatis-Plus:
同样可以很好实现
一百零二、Service 层
MyBatis-Plus 还提供:
IService<T>
一百零三、接口
public interface StudentService
extends IService<Student> {
}
一百零四、实现
@Service
public class StudentServiceImpl
extends ServiceImpl<
StudentMapper,
Student
>
implements StudentService {
}
一百零五、为什么这样写
ServiceImpl 已经:
持有 Mapper
并实现:
IService 中大量通用方法
一百零六、常用 IService 方法
例如:
save
saveBatch
removeById
updateById
getById
list
count
page
一百零七、新增
studentService.save(
student
);
一百零八、根据 ID 查询
studentService.getById(
1L
);
一百零九、列表
studentService.list(
wrapper
);
一百一十、删除
studentService.removeById(
1L
);
一百一十一、修改
studentService.updateById(
student
);
一百一十二、批量保存
studentService.saveBatch(
list
);
一百一十三、为什么 Service 层不要只为了“调用 Mapper”存在
错误:
public Student get(Long id) {
return studentMapper
.selectById(id);
}
所有 Service:
只有一行
价值很低。
一百一十四、真正 Service 应该放什么
业务规则
事务
跨表逻辑
权限
状态
校验
一百一十五、IService 是工具
不是:
让你删除 Service 设计
一百一十六、ServiceImpl 中访问 Mapper
通常可以:
baseMapper
一百一十七、例如
Student student =
baseMapper.selectById(
id
);
一百一十八、但是复杂 Mapper 方法
也可以正常定义:
StudentDetailVO
selectStudentDetail(
Long id
);
然后:
baseMapper
.selectStudentDetail(id);
一百一十九、所以继承 BaseMapper 后仍可以自定义方法
非常重要。
一百二十、Mapper
public interface StudentMapper
extends BaseMapper<Student> {
StudentDetailVO
selectStudentDetail(
@Param("id")
Long id
);
}
一百二十一、XML
<select
id="selectStudentDetail"
resultType="...StudentDetailVO"
>
SELECT
s.id,
s.name,
c.class_name
FROM sys_student s
LEFT JOIN sys_class c
ON c.id = s.class_id
WHERE s.id = #{id}
</select>
一百二十二、这就是 MP + MyBatis 混合开发
简单 SQL
→ BaseMapper
复杂 SQL
→ XML
一百二十三、这才是最推荐的企业用法
不要:
强迫所有复杂 SQL 都用 Wrapper
一百二十四、为什么
复杂 Wrapper:
可读性可能比 SQL 更差
一百二十五、例如 6 表 JOIN
直接:
XML SQL
通常更清晰。
一百二十六、什么时候该写 XML
可以记:
多表 JOIN
复杂统计
复杂子查询
复杂报表
复杂 DataScope
数据库特定 SQL
一百二十七、分页
MyBatis-Plus 提供:
PaginationInnerInterceptor
一百二十八、插件配置
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor
mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor =
new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(
DbType.MYSQL
)
);
return interceptor;
}
}
一百二十九、为什么指定 DbType.MYSQL
可以帮助:
分页 SQL 方言识别
一百三十、多个 InnerInterceptor 时
分页插件:
通常建议放在后面
因为其他插件:
可能先修改 SQL
一百三十一、分页对象
Page<Student> page =
new Page<>(
1,
10
);
表示:
第 1 页
每页 10 条
一百三十二、Mapper 分页
IPage<Student> result =
studentMapper
.selectPage(
page,
wrapper
);
一百三十三、结果
result.getRecords();
数据。
一百三十四、
result.getTotal();
总数。
一百三十五、
result.getCurrent();
当前页。
一百三十六、
result.getSize();
每页数量。
一百三十七、
result.getPages();
总页数。
一百三十八、Service 分页
Page<Student> page =
new Page<>(
pageNum,
pageSize
);
IPage<Student> result =
studentService.page(
page,
wrapper
);
一百三十九、为什么分页插件找不到
首先检查:
MyBatis-Plus 版本
如果:
3.5.9+
再检查:
mybatis-plus-jsqlparser
一百四十、为什么这是当前版本重点
官方已经把分页相关解析模块:
拆成可选依赖
旧教程:
可能没有这一条
一百四十一、分页最大页大小
PaginationInnerInterceptor:
支持 maxLimit
可以限制:
单页最大数量
一百四十二、为什么要限制
前端如果传:
pageSize = 1000000
可能:
拖垮数据库
一百四十三、建议
例如:
100
500
根据业务决定。
一百四十四、分页 count 优化
大数据复杂 JOIN:
COUNT
有时本身很慢。
这时候:
需要针对 SQL 优化
不能认为:
用了分页插件就一定快
一百四十五、逻辑删除
现实项目中:
很多数据不能真的 DELETE
例如:
服务项目
用户
业务配置
一百四十六、逻辑删除意思
数据库行:
仍然存在
只是字段:
deleted = 1
一百四十七、未删除
deleted = 0
一百四十八、实体
@TableLogic
private Integer deleted;
一百四十九、删除
studentMapper
.deleteById(id);
使用逻辑删除后,
实际效果类似:
UPDATE sys_student
SET deleted = 1
WHERE
id = ?
AND deleted = 0;
一百五十、查询
会自动附加:
deleted = 0
一百五十一、逻辑删除配置
也可以全局:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
一百五十二、全局配置和 @TableLogic 怎么选
统一项目字段:
全局配置
特殊单表:
@TableLogic
一百五十三、逻辑删除不是万能的
对于真正无历史价值的:
临时表
可能:
物理删除更合理
一百五十四、最重要原则
逻辑删除的效果应该:
从业务上等价于删除
一百五十五、逻辑删除后唯一索引问题
例如:
username UNIQUE
逻辑删除用户:
username 仍然存在
再次新增同用户名:
可能唯一冲突
一百五十六、所以逻辑删除必须和唯一性一起设计
不要:
只加 deleted 字段就结束
一百五十七、方案需要根据业务决定
例如:
联合唯一键
删除时改业务唯一值
使用时间型逻辑删除值
一百五十八、自动填充
常见字段:
createTime
updateTime
createUserId
updateUserId
每次手写:
很烦
一百五十九、MyBatis-Plus 提供
MetaObjectHandler
一百六十、字段
@TableField(
fill = FieldFill.INSERT
)
private LocalDateTime createTime;
@TableField(
fill = FieldFill.INSERT_UPDATE
)
private LocalDateTime updateTime;
一百六十一、Handler
@Component
public class MyMetaObjectHandler
implements MetaObjectHandler {
@Override
public void insertFill(
MetaObject metaObject
) {
this.strictInsertFill(
metaObject,
"createTime",
LocalDateTime.class,
LocalDateTime.now()
);
this.strictInsertFill(
metaObject,
"updateTime",
LocalDateTime.class,
LocalDateTime.now()
);
}
@Override
public void updateFill(
MetaObject metaObject
) {
this.strictUpdateFill(
metaObject,
"updateTime",
LocalDateTime.class,
LocalDateTime.now()
);
}
}
一百六十二、为什么 Handler 必须交给 Spring 管理
要:
@Component
或:
@Bean
否则:
不会生效
一百六十三、自动填充的一个大坑
如果你调用:
update(wrapper)
没有实体对象,
某些自动填充场景:
不会触发
一百六十四、官方特别提醒
类似:
update(entity, wrapper)
如果:
entity == null
自动填充:
可能无法工作
一百六十五、所以不要认为
加了 MetaObjectHandler
所有 update 都一定自动填充
一百六十六、企业里使用更新时间
条件更新时:
可以明确 set updateTime
或者:
传实体触发填充
一百六十七、自动填充当前用户
例如:
createUserId
一百六十八、Handler
Long userId =
SecurityUtils.getUserId();
然后:
strictInsertFill
一百六十九、但要考虑非登录线程
例如:
定时任务
初始化脚本
消息消费
可能:
没有 LoginUser
一百七十、因此
获取用户:
要做空值兼容
一百七十一、乐观锁
用于解决:
并发修改覆盖
一百七十二、例子
用户 A:
读取库存 version=1
用户 B:
也读取 version=1
一百七十三、A 更新
SQL 类似:
UPDATE product
SET
stock = 9,
version = 2
WHERE
id = 1
AND version = 1;
成功。
一百七十四、B 更新
仍然:
version = 1
SQL:
更新 0 行
说明:
数据已被别人改过
一百七十五、MyBatis-Plus 乐观锁插件
OptimisticLockerInnerInterceptor
一百七十六、配置
@Bean
public MybatisPlusInterceptor
mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor =
new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(
new OptimisticLockerInnerInterceptor()
);
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(
DbType.MYSQL
)
);
return interceptor;
}
一百七十七、实体版本字段
@Version
private Integer version;
一百七十八、数据库
version INT NOT NULL DEFAULT 0
一百七十九、查询实体
Student student =
studentMapper.selectById(id);
一百八十、修改
student.setName(
"新名字"
);
int rows =
studentMapper
.updateById(student);
一百八十一、如果版本已变化
rows = 0
应该:
提示用户数据已被修改,请刷新
一百八十二、为什么不能自动覆盖
否则:
会丢失另一个人的修改
一百八十三、乐观锁适合什么
读多写少
冲突概率不高
一百八十四、不适合什么
极高冲突:
库存秒杀
仅靠普通乐观锁:
可能大量重试
需要结合:
原子 SQL
缓存
队列
一百八十五、乐观锁和前面 oldStatus 条件更新关系
本质思想很像:
我更新时
要求数据仍是我读取时的状态
一百八十六、oldStatus 是业务版本
@Version:
是通用版本号
一百八十七、两者不一定同时用
简单状态流转:
oldStatus 条件
通常已经很好。
复杂实体多人编辑:
@Version
很适合。
一百八十八、主键策略
常见:
AUTO
ASSIGN_ID
ASSIGN_UUID
一百八十九、AUTO
数据库:
自增
一百九十、ASSIGN_ID
MyBatis-Plus:
生成数值型唯一 ID
一百九十一、ASSIGN_UUID
生成:
UUID 字符串
一百九十二、为什么表主键不建议超长 UUID 字符串随便做聚簇主键
MySQL InnoDB:
主键会影响索引存储
超长随机字符串:
索引占用更大
插入局部性差
一百九十三、课程项目推荐
BIGINT
一百九十四、@TableField
用途很多。
例如:
@TableField("user_name")
private String username;
一百九十五、字段不存在数据库
@TableField(
exist = false
)
private String deptName;
一百九十六、为什么
deptName:
只用于业务展示
数据库表:
没有这一列
一百九十七、但企业项目更推荐
展示字段:
放 VO
而不是:
全塞 Entity
一百九十八、exist=false 适合
少量:
临时非表字段
一百九十九、VO 更清晰
复杂查询:
StudentDetailVO
二百、字段策略
例如:
insertStrategy
updateStrategy
whereStrategy
用于控制:
null
空字符串
字段是否参与 SQL
二百零一、为什么不要全局乱改策略
可能:
影响所有实体
二百零二、优先:
局部明确配置
二百零三、selectOne
用于:
预期只返回一条
二百零四、例如
Student student =
studentMapper
.selectOne(
Wrappers
.<Student>
lambdaQuery()
.eq(
Student::getPhone,
"13800138000"
)
);
二百零五、如果返回多条怎么办
通常会:
抛多结果异常
所以:
selectOne 只用于真正唯一条件
二百零六、不要靠
selectOne
掩盖数据库缺唯一约束。
二百零七、例如 phone 真正唯一
数据库:
UNIQUE(phone)
二百零八、count
Long count =
studentMapper
.selectCount(
wrapper
);
二百零九、exists
如果只是判断是否存在,
新版本可能提供:
exists
相关能力。
实际:
以当前 Mapper/Service API 为准
二百一十、不要为了 exists 查整行
错误:
selectList
然后 size > 0
二百一十一、简单统计
count
更合理。
二百一十二、批量操作
批量新增:
studentService
.saveBatch(list);
二百一十三、为什么比循环 insert 更好
减少:
大量 Java 调用和数据库交互开销
二百一十四、但“saveBatch”不等于所有数据库场景都是单 SQL
实际底层行为:
需要看版本和执行器
二百一十五、真正超大批量
需要考虑:
分批
JDBC rewriteBatchedStatements
数据库连接参数
事务大小
二百一十六、不要一次 saveBatch 100 万条
二百一十七、建议分片
例如:
500
1000
5000
根据实际压测。
二百一十八、LambdaQueryChainWrapper
Service 中常见:
studentService
.lambdaQuery()
.eq(
Student::getGender,
1
)
.list();
二百一十九、为什么方便
省掉:
new LambdaQueryWrapper
二百二十、LambdaUpdateChainWrapper
例如:
studentService
.lambdaUpdate()
.eq(
Student::getId,
id
)
.set(
Student::getName,
name
)
.update();
二百二十一、什么时候用 Chain Wrapper
Service 内简单查询:
很方便
二百二十二、什么时候不要
复杂查询:
为了可读性
单独:
buildWrapper()
更清楚。
二百二十三、推荐 buildWrapper
private LambdaQueryWrapper<Student>
buildQueryWrapper(
StudentQueryDTO query
) {
return Wrappers
.<Student>
lambdaQuery()
.like(
StringUtils.hasText(
query.getName()
),
Student::getName,
query.getName()
)
.eq(
query.getGender()
!= null,
Student::getGender,
query.getGender()
)
.ge(
query.getMinAge()
!= null,
Student::getAge,
query.getMinAge()
)
.le(
query.getMaxAge()
!= null,
Student::getAge,
query.getMaxAge()
)
.orderByDesc(
Student::getCreateTime
);
}
二百二十四、为什么好
Controller:
干净
Service:
查询规则集中
二百二十五、Wrapper 与 SQL 索引
使用 Wrapper:
不代表不用懂索引
二百二十六、例如
wrapper.like(
User::getName,
keyword
);
对应:
%keyword%
普通 B+Tree 索引:
可能难以高效利用
二百二十七、所以 MP 只是 SQL 生成工具
数据库原理:
仍然必须学
二百二十八、Wrapper 不会自动帮你设计索引
二百二十九、Wrapper 也不会自动避免慢 SQL
二百三十、MyBatis-Plus 插件体系
核心:
MybatisPlusInterceptor
里面:
添加 InnerInterceptor
二百三十一、常见插件
PaginationInnerInterceptor
OptimisticLockerInnerInterceptor
TenantLineInnerInterceptor
BlockAttackInnerInterceptor
IllegalSQLInnerInterceptor
二百三十二、多租户插件
TenantLineInnerInterceptor
可以:
自动追加 tenant_id 条件
二百三十三、适合
SaaS:
多个租户共用数据库结构
二百三十四、当前课程项目
先:
了解
二百三十五、BlockAttackInnerInterceptor
可以防止部分:
全表 UPDATE
全表 DELETE
二百三十六、为什么有价值
例如误写:
UPDATE user
SET status = 0;
没有:
WHERE
非常危险。
二百三十七、但插件不是万能保险
生产:
仍要 Review
二百三十八、代码生成器
MyBatis-Plus 提供:
Generator
现代版本推荐:
FastAutoGenerator
二百三十九、需要额外依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.17</version>
</dependency>
二百四十、模板引擎
还要根据选择:
Velocity
Freemarker
Beetl
Enjoy
二百四十一、为什么生成器没有把所有模板引擎强制带进来
避免:
不需要的依赖
二百四十二、FastAutoGenerator 示例
FastAutoGenerator
.create(
url,
username,
password
)
.globalConfig(
builder ->
builder
.author("Haruko")
.outputDir(
System
.getProperty(
"user.dir"
)
+
"/src/main/java"
)
)
.packageConfig(
builder ->
builder
.parent(
"com.example"
)
.moduleName(
"student"
)
)
.strategyConfig(
builder ->
builder
.addInclude(
"sys_student"
)
.addTablePrefix(
"sys_"
)
)
.execute();
二百四十三、生成什么
通常:
Entity
Mapper
Mapper XML
Service
ServiceImpl
Controller
二百四十四、为什么代码生成后仍要 Review
生成器只知道:
数据库结构
不知道:
真实业务规则
二百四十五、生成出来的 Controller
可能只是:
CRUD
你仍然要加:
权限
DTO
VO
事务
业务状态
二百四十六、不要把代码生成器理解为
自动完成项目
二百四十七、正确理解
自动生成重复骨架
二百四十八、MyBatis-Plus 与若依
这里一定要分情况。
标准 RuoYi-Vue:
通常核心数据访问仍以原生 MyBatis
和 Mapper XML 为主
二百四十九、如果你自己给若依业务模块加 MP
可以。
但要:
统一依赖
统一 MapperScan
统一分页方案
统一实体基类
统一逻辑删除策略
二百五十、最怕什么
一个项目里:
若依 PageHelper
MyBatis-Plus Page
两个分页思路乱混
二百五十一、能不能共存
技术上:
可以
但每个接口:
要清楚自己使用哪套
二百五十二、例如若依原页面
Controller:
startPage();
Mapper:
XML 查询
继续:
PageHelper
二百五十三、MP 新业务单表页
可以:
Page<T>
+
selectPage
二百五十四、为什么不要同一个查询同时用
startPage()
+
new Page<>()
容易:
分页逻辑冲突
二百五十五、团队最好统一约定
例如:
若依原模块
→ PageHelper
新增 MP 模块
→ MP Pagination
二百五十六、或者全部业务模块仍用 PageHelper
MyBatis-Plus 只:
减少 BaseMapper CRUD
也可以。
二百五十七、工具服从架构
不要:
为了学 MP
强行改整个若依
二百五十八、MyBatis-Plus 与 DataScope
若依 DataScope 常见:
AOP
+
params.dataScope
+
XML
二百五十九、如果你完全改成 Wrapper
原来的:
${params.dataScope}
可能:
没地方拼
二百六十、这就是为什么复杂权限 SQL
原生 XML:
仍然有优势
二百六十一、不要为了“全 MP”
把成熟 DataScope:
重写一遍
二百六十二、推荐
简单单表:
MP Wrapper
复杂 DataScope:
XML
二百六十三、MyBatis-Plus 与事务
MP:
不会替你自动解决业务事务
二百六十四、例如
save(order);
saveItems(items);
两个表:
仍然要 @Transactional
二百六十五、MyBatis-Plus 只是 DAO 增强
事务:
还是 Spring 事务
二百六十六、MyBatis-Plus 与 Redis
没有直接替代关系。
MP
→ 数据库访问
Redis
→ 缓存 / 分布式数据结构
二百六十七、不要混淆
二百六十八、MyBatis-Plus 与 JPA
两者都能:
减少 CRUD
但设计思想:
不同
二百六十九、MP
仍然基于:
MyBatis
SQL 控制:
更直接
二百七十、JPA
更强调:
ORM / Entity Relationship
二百七十一、Java 国内后端项目里
MyBatis / MyBatis-Plus:
非常常见
二百七十二、MyBatis-Plus 常见错误 1
报:
Invalid bound statement
先检查:
MapperScan
Mapper 是否被 Spring 扫描
XML namespace
XML 路径
二百七十三、错误 2:表名找不到
检查:
@TableName
数据库
schema
二百七十四、错误 3:Unknown column
检查:
实体字段
@TableField
数据库字段
二百七十五、如果实体有非表字段
加:
@TableField(exist = false)
或更推荐:
移到 VO
二百七十六、错误 4:主键不回填
检查:
@TableId
IdType
数据库自增
二百七十七、错误 5:逻辑删除不生效
检查:
@TableLogic
全局配置
字段值
二百七十八、错误 6:删除后自己写 SQL 还能查出来
如果自定义 XML:
你自己写的 SQL
要确认:
是否自动受逻辑删除规则影响
不要想当然。
复杂自定义 SQL:
最好明确条件
二百七十九、错误 7:分页无效
检查:
MybatisPlusInterceptor Bean
PaginationInnerInterceptor
jsqlparser 依赖
是否真的走 MP 分页
二百八十、错误 8:分页查全部数据
可能:
插件没配置
二百八十一、错误 9:Wrapper 条件没加上
检查:
condition boolean
是不是:
false
二百八十二、错误 10:Lambda 字段报错
检查:
是否实体 getter 正常
是否字段映射正确
二百八十三、错误 11:updateById 无法置 null
检查:
字段更新策略
可以考虑:
LambdaUpdateWrapper.set(field, null)
二百八十四、错误 12:自动填充不生效
检查:
@TableField(fill=...)
MetaObjectHandler
@Component
update 是否带 entity
二百八十五、错误 13:乐观锁完全没作用
检查:
@Version
OptimisticLockerInnerInterceptor
实体是否携带旧 version
二百八十六、错误 14:乐观锁更新 0 行还提示成功
Service:
没有检查 rows
二百八十七、错误 15:selectOne 多条报错
因为条件:
并不唯一
正确:
数据库加 UNIQUE
二百八十八、错误 16:复杂 Wrapper 看不懂
这不是:
你能力差
而是:
可能已经该写 XML
二百八十九、什么时候切换 XML
如果 Wrapper 已经出现:
大量 nested
apply
exists
inSql
复杂 select
复杂 JOIN
先问:
直接 SQL 是否更清楚
二百九十、企业项目不是“Wrapper 越多越高级”
二百九十一、完整 CRUD 示例:DTO
@Data
public class StudentSaveDTO {
@NotBlank
private String name;
@Min(0)
@Max(150)
private Integer age;
private Integer gender;
private String phone;
}
二百九十二、Query DTO
@Data
public class StudentQueryDTO {
private String name;
private Integer minAge;
private Integer maxAge;
private Integer gender;
private Integer pageNum = 1;
private Integer pageSize = 10;
}
二百九十三、VO
@Data
public class StudentVO {
private Long id;
private String name;
private Integer age;
private String genderName;
private String phone;
private LocalDateTime createTime;
}
二百九十四、为什么 DTO / VO 不省
MyBatis-Plus:
省的是数据库 CRUD
不是:
让接口直接暴露 Entity
二百九十五、Entity
@Data
@TableName("sys_student")
public class Student {
@TableId(
type = IdType.AUTO
)
private Long id;
private String name;
private Integer age;
private Integer gender;
private String phone;
@TableField(
fill = FieldFill.INSERT
)
private LocalDateTime createTime;
@TableField(
fill = FieldFill.INSERT_UPDATE
)
private LocalDateTime updateTime;
@Version
private Integer version;
@TableLogic
private Integer deleted;
}
二百九十六、Mapper
@Mapper
public interface StudentMapper
extends BaseMapper<Student> {
}
二百九十七、Service
public interface StudentService
extends IService<Student> {
IPage<StudentVO>
selectStudentPage(
StudentQueryDTO query
);
}
二百九十八、ServiceImpl
@Service
public class StudentServiceImpl
extends ServiceImpl<
StudentMapper,
Student
>
implements StudentService {
@Override
public IPage<StudentVO>
selectStudentPage(
StudentQueryDTO query
) {
Page<Student> page =
new Page<>(
query.getPageNum(),
query.getPageSize()
);
LambdaQueryWrapper<Student>
wrapper =
Wrappers
.lambdaQuery(
Student.class
)
.like(
StringUtils
.hasText(
query.getName()
),
Student::getName,
query.getName()
)
.ge(
query.getMinAge()
!= null,
Student::getAge,
query.getMinAge()
)
.le(
query.getMaxAge()
!= null,
Student::getAge,
query.getMaxAge()
)
.eq(
query.getGender()
!= null,
Student::getGender,
query.getGender()
)
.orderByDesc(
Student::getCreateTime
);
IPage<Student> entityPage =
baseMapper.selectPage(
page,
wrapper
);
Page<StudentVO> voPage =
new Page<>(
entityPage.getCurrent(),
entityPage.getSize(),
entityPage.getTotal()
);
List<StudentVO> vos =
entityPage
.getRecords()
.stream()
.map(
this::toVO
)
.toList();
voPage.setRecords(
vos
);
return voPage;
}
}
二百九十九、为什么分页后转 VO
避免:
Controller 直接返回 Entity
三百、Controller
@RestController
@RequestMapping(
"/student"
)
public class StudentController {
private final StudentService
studentService;
public StudentController(
StudentService studentService
) {
this.studentService =
studentService;
}
@GetMapping("/page")
public IPage<StudentVO> page(
StudentQueryDTO query
) {
return studentService
.selectStudentPage(
query
);
}
}
三百零一、新增 Service 方法
@Transactional(
rollbackFor = Exception.class
)
public Long create(
StudentSaveDTO dto
) {
Student student =
new Student();
student.setName(
dto.getName()
);
student.setAge(
dto.getAge()
);
student.setGender(
dto.getGender()
);
student.setPhone(
dto.getPhone()
);
save(student);
return student.getId();
}
三百零二、为什么不用 BeanUtils 无脑 copy
简单项目可以。
但要注意:
前端 DTO 字段
不一定等于 Entity 所有字段
尤其:
id
status
deleted
version
createTime
不应该:
被前端随便覆盖
三百零三、企业里可以用
MapStruct
做安全对象转换。
当前:
先手动理解
三百零四、修改
@Transactional(
rollbackFor = Exception.class
)
public void update(
Long id,
StudentSaveDTO dto
) {
Student student =
getById(id);
if (
student == null
) {
throw new ServiceException(
"学生不存在"
);
}
student.setName(
dto.getName()
);
student.setAge(
dto.getAge()
);
student.setGender(
dto.getGender()
);
student.setPhone(
dto.getPhone()
);
boolean success =
updateById(student);
if (
!success
) {
throw new ServiceException(
"更新失败,请刷新后重试"
);
}
}
三百零五、为什么先查询
可能要:
校验存在
检查旧状态
使用乐观锁 version
三百零六、删除
public void delete(
Long id
) {
boolean success =
removeById(id);
if (
!success
) {
throw new ServiceException(
"删除失败或数据不存在"
);
}
}
三百零七、如果启用 @TableLogic
这里:
就是逻辑删除
三百零八、MyBatis-Plus 与 RBAC
权限:
和 MP 没直接关系
三百零九、Controller 仍然:
@PreAuthorize(...)
三百一十、数据范围
仍然:
需要业务层 / SQL 层控制
三百一十一、不要认为 Wrapper 会自动防越权
三百一十二、比如修改学生
前端:
id = 100
后端直接:
updateById(entity)
如果没有:
Ownership / DataScope
仍然可能:
水平越权
三百一十三、所以 MP 解决的是
写 SQL 的重复工作
不是:
业务安全
三百一十四、MyBatis-Plus 与数据字典
状态字段:
仍用 code
前端:
字典展示
三百一十五、不要把中文状态直接存数据库
例如:
审批中
更推荐:
2
或:
PROCESSING
三百一十六、代码里使用枚举
三百一十七、MyBatis-Plus 与复杂 Join
官方核心 Wrapper:
主要围绕单表
三百一十八、多表查询
最稳定:
自定义 Mapper XML
三百一十九、虽然生态里有扩展 Join 插件
但它不是:
MyBatis-Plus 核心必须知识
课程先不依赖。
三百二十、为什么
先学会:
标准 MP + 标准 MyBatis
更通用。
三百二十一、MyBatis-Plus 代码生成器与若依代码生成器区别
若依:
本身也有代码生成
三百二十二、若依 Generator
通常能生成:
后端
Vue 页面
菜单 SQL
更适合:
若依项目整体 CRUD
三百二十三、MyBatis-Plus Generator
更偏:
Java 数据访问骨架
三百二十四、若依项目该用哪个
如果你正在:
标准若依项目
优先:
若依自带生成器
三百二十五、普通 Spring Boot + MP 项目
可以:
FastAutoGenerator
三百二十六、不要同时生成两套同名类
三百二十七、MyBatis-Plus 常见面试题 1
问:
MyBatis-Plus 和 MyBatis 什么关系?
答:
MyBatis-Plus 是基于 MyBatis 的增强工具,
保留 MyBatis 原有能力的同时提供通用 Mapper、
Service、条件构造器、分页、逻辑删除和乐观锁等功能。
复杂 SQL 仍然可以继续使用原生 MyBatis Mapper XML。
三百二十八、面试题 2:BaseMapper 有什么作用
答:
BaseMapper 是 MyBatis-Plus 提供的通用 Mapper 接口。
业务 Mapper 继承 BaseMapper<Entity> 后,
可以直接获得常见的 insert、delete、update、select、
count 和 page 等 CRUD 能力,
减少重复 Mapper XML。
三百二十九、面试题 3:为什么推荐 LambdaQueryWrapper
答:
普通 QueryWrapper 经常使用字符串列名,
字段重构后编译器无法检查。
LambdaQueryWrapper 使用 Entity::getXxx 方法引用,
具有更好的类型安全和 IDE 重构支持,
能减少字段拼写错误。
三百三十、面试题 4:MyBatis-Plus 是否可以完全不写 XML
答:
不建议这样理解。
简单单表 CRUD 使用 MyBatis-Plus 很方便,
但复杂 JOIN、报表、统计、复杂 DataScope
使用 Mapper XML 往往更清晰。
企业项目通常两者结合。
三百三十一、面试题 5:逻辑删除原理
答:
逻辑删除不会真正 DELETE 数据,
而是把删除标记字段改成已删除状态。
之后普通查询会自动过滤已删除数据,
删除操作也会转换为 UPDATE。
但逻辑删除还需要考虑唯一索引和历史数据设计。
三百三十二、面试题 6:乐观锁原理
答:
在表中增加 version 字段。
更新时 SQL 同时校验旧 version,
只有版本仍一致才允许更新,
更新成功后 version 增加。
如果更新行数为 0,
说明数据已经被其他事务修改。
三百三十三、面试题 7:乐观锁适合什么
答:
适合读多写少、并发冲突概率较低的场景。
高冲突场景如果频繁失败和重试,
通常还需要使用原子 SQL、队列或其他并发方案。
三百三十四、面试题 8:自动填充怎么实现
答:
实体字段使用 @TableField(fill = FieldFill.xxx)
声明填充时机,
然后实现 MetaObjectHandler,
在 insertFill 和 updateFill 中设置创建时间、
更新时间、创建人等字段。
三百三十五、面试题 9:为什么 update 自动填充有时失效
答:
自动填充依赖实体对象的元数据和填充字段。
某些只使用 Wrapper、不提供 entity 的更新方式,
不会按照预期触发实体字段自动填充。
因此条件更新时要明确当前调用方式,
必要时显式 set 更新时间或提供实体对象。
三百三十六、面试题 10:分页怎么实现
答:
配置 MybatisPlusInterceptor,
加入 PaginationInnerInterceptor,
再使用 Page/IPage 配合 selectPage 或 Service.page。
MyBatis-Plus 3.5.9+ 中分页 SQL 解析模块已拆分,
还需要确认 mybatis-plus-jsqlparser 依赖。
三百三十七、面试题 11:Wrapper 有 SQL 注入风险吗
答:
正常使用 eq、ge、like 等参数化 API 风险较低,
但 last、apply、inSql 等允许 SQL 片段的能力需要谨慎。
不能把客户端提交的 SQL 字符串直接拼入 Wrapper,
也不应该把 Wrapper 作为 Controller 的远程请求模型。
三百三十八、面试题 12:为什么 selectOne 条件应该唯一
答:
selectOne 表达的是“业务上最多一条”。
如果数据库实际返回多条,
通常会抛多结果异常。
真正要求唯一的数据还应该建立数据库 UNIQUE 约束,
不能只靠 selectOne 假设唯一。
三百三十九、面试题 13:IService 有什么作用
答:
IService 提供 Service 层通用 CRUD 能力,
ServiceImpl 可以快速实现这些方法。
但真正业务 Service 仍应该负责事务、
状态、权限和跨表业务,
不能把 IService 当成取消业务层设计的理由。
三百四十、面试题 14:MyBatis-Plus 和 PageHelper 能一起用吗
答:
技术上可以共存,
但一个具体查询最好明确使用一套分页机制。
若依原有 Mapper XML 可以继续使用 PageHelper,
MP 新模块可以使用 Page + PaginationInnerInterceptor。
不要对同一个查询同时 startPage() 又传 Page 对象。
三百四十一、面试题 15:MyBatis-Plus 如何和若依配合
答:
若依原有模块可以继续保留 MyBatis XML 和 PageHelper。
新增业务模块如果需要可以引入 MyBatis-Plus,
简单单表 CRUD 用 BaseMapper/Wrapper,
复杂 DataScope、多表 JOIN 继续写 XML。
不要为了使用 MP 而重写若依成熟的权限和分页体系。
三百四十二、面试题 16:为什么 Wrapper 不是越复杂越好
答:
Wrapper 的价值是简化常见查询。
如果一个查询包含大量嵌套、子查询、
SQL 片段和多表逻辑,
继续使用 Wrapper 可能降低可读性。
这种情况下直接写清晰的 Mapper XML SQL
通常更容易维护和优化。
三百四十三、面试题 17:MP 能自动解决事务吗
答:
不能。
MyBatis-Plus 主要增强数据库访问层。
多个写操作需要整体成功时,
仍然应该使用 Spring @Transactional
保证业务事务一致性。
三百四十四、面试题 18:为什么实体不建议直接作为所有 API DTO
答:
Entity 表示数据库结构,
而请求 DTO 表示客户端允许提交的数据。
如果直接接收 Entity,
客户端可能提交 id、status、deleted、version
等不应该修改的字段。
DTO/VO 可以建立更安全清晰的接口边界。
三百四十五、面试题 19:IdType.AUTO 与 ASSIGN_ID 区别
答:
AUTO 主要依赖数据库自增主键。
ASSIGN_ID 由 MyBatis-Plus 生成数值型唯一 ID,
更适合需要在插入数据库前就获得 ID
或分布式场景中的主键生成。
三百四十六、面试题 20:MP 最大价值是什么
答:
减少重复的单表 CRUD 和动态条件 SQL,
提高开发效率,
同时保留 MyBatis 对复杂 SQL 的控制能力。
正确使用方式不是“无 SQL 化”,
而是让简单问题简单解决,复杂问题仍保持可控。
三百四十七、MyBatis-Plus 知识树
MyBatis-Plus
│
├─ Core
│ ├─ BaseMapper
│ ├─ IService
│ ├─ ServiceImpl
│ └─ Wrappers
│
├─ Entity
│ ├─ @TableName
│ ├─ @TableId
│ ├─ @TableField
│ ├─ @TableLogic
│ └─ @Version
│
├─ Query
│ ├─ QueryWrapper
│ ├─ LambdaQueryWrapper
│ ├─ eq
│ ├─ like
│ ├─ in
│ ├─ between
│ ├─ and/or
│ └─ orderBy
│
├─ Update
│ ├─ updateById
│ ├─ UpdateWrapper
│ ├─ LambdaUpdateWrapper
│ └─ oldStatus Condition
│
├─ Page
│ ├─ Page
│ ├─ IPage
│ ├─ PaginationInnerInterceptor
│ └─ mybatis-plus-jsqlparser
│
├─ Data Management
│ ├─ Logic Delete
│ ├─ Auto Fill
│ ├─ Optimistic Lock
│ └─ ID Strategy
│
├─ Generator
│ ├─ FastAutoGenerator
│ ├─ Entity
│ ├─ Mapper
│ ├─ Service
│ └─ Controller
│
├─ MyBatis
│ ├─ Mapper XML
│ ├─ Complex JOIN
│ ├─ Report SQL
│ └─ DataScope
│
└─ Enterprise
├─ DTO / VO
├─ @Transactional
├─ RBAC
├─ Ownership
├─ Index
└─ SQL Security
三百四十八、最重要的 API 记忆图
Mapper 层
↓
BaseMapper<T>
↓
insert
deleteById
updateById
selectById
selectList
selectCount
selectPage
三百四十九、Service 层
IService<T>
+
ServiceImpl<M,T>
↓
save
saveBatch
getById
list
page
updateById
removeById
三百五十、Query
LambdaQueryWrapper
↓
eq
ne
gt
ge
lt
le
like
between
in
isNull
and
or
orderBy
三百五十一、Update
LambdaUpdateWrapper
↓
eq old condition
↓
set new value
↓
update
↓
check affected rows
三百五十二、分页
MybatisPlusInterceptor
↓
PaginationInnerInterceptor
↓
Page<T>
↓
selectPage
↓
IPage<T>
三百五十三、自动填充
@TableField(fill=...)
↓
MetaObjectHandler
↓
insertFill / updateFill
三百五十四、逻辑删除
@TableLogic
↓
deleteById
↓
UPDATE deleted=1
↓
普通查询自动 deleted=0
三百五十五、乐观锁
@Version
↓
OptimisticLockerInnerInterceptor
↓
UPDATE
WHERE id=?
AND version=oldVersion
↓
version + 1
三百五十六、MP 与 MyBatis 最佳组合
单表 CRUD
↓
MyBatis-Plus
复杂 JOIN / 报表 / DataScope
↓
Mapper XML
两者共存
↓
企业项目
三百五十七、练习 1:学生 CRUD
要求:
新增学生
修改学生
删除学生
按 ID 查询
查询全部
使用:
BaseMapper
三百五十八、练习 2:条件查询
条件:
姓名模糊
年龄范围
性别
使用:
LambdaQueryWrapper
三百五十九、练习 3:动态条件
如果:
name 为空
不要:
添加 name 条件
使用:
Wrapper condition 参数
三百六十、练习 4:分页
实现:
pageNum
pageSize
条件查询
total
records
三百六十一、练习 5:逻辑删除
增加:
deleted
然后验证:
deleteById 后数据库行仍存在
普通 select 查不到
三百六十二、练习 6:自动填充
实现:
createTime
updateTime
三百六十三、练习 7:乐观锁
两个线程/两个请求:
读取同一 version
先后修改:
第二个更新失败
三百六十四、练习 8:状态条件更新
订单:
DRAFT
→
SUBMITTED
使用:
LambdaUpdateWrapper
带:
oldStatus
三百六十五、练习 9:复杂 JOIN
不要 Wrapper。
写:
Mapper XML
查询:
学生 + 班级
三百六十六、练习 10:生成器
使用:
FastAutoGenerator
生成:
Student
Mapper
Service
Controller
然后:
人工 Review
三百六十七、实际项目书写顺序
拿到一张新表:
1. 建表
三百六十八、
2. 写 Entity
三百六十九、
3. 写 Mapper extends BaseMapper
三百七十、
4. 先测试 selectById / insert
三百七十一、
5. 写 Service / ServiceImpl
三百七十二、
6. 定义 DTO / VO
三百七十三、
7. buildWrapper
三百七十四、
8. 写分页
三百七十五、
9. 写 Controller
三百七十六、
10. 加权限 / 事务 / 校验
三百七十七、
11. 复杂 SQL 再写 XML
三百七十八、
12. 测试
三百七十九、IDEA 快捷键
创建类:
Alt + Insert
三百八十、搜索类:
Ctrl + N
三百八十一、搜索文件:
Ctrl + Shift + N
三百八十二、全局搜索:
Ctrl + Shift + F
三百八十三、跳定义:
Ctrl + B
三百八十四、查实现:
Ctrl + Alt + B
三百八十五、查用法:
Alt + F7
三百八十六、为什么学 MP 特别要会跳源码
很多时候你会问:
BaseMapper 到底有哪些方法?
最准确:
Ctrl+B 看当前版本源码
三百八十七、不要只靠网上博客记 API
版本:
会变化
三百八十八、Cursor 提示词:集成
当前项目是 Spring Boot 3 + JDK17 + MySQL。
请先读取 pom.xml,不修改。
我要引入 MyBatis-Plus。
请检查:
1. 当前 Spring Boot 版本
2. 当前是否已经有 MyBatis Starter
3. 是否存在版本冲突
4. Spring Boot3 应使用哪个 MP Starter
5. 当前是否需要 mybatis-plus-jsqlparser
6. MapperScan 是否需要调整
7. mapper-locations 是否兼容现有 XML
先给最小集成方案,不要删除现有复杂 Mapper XML。
三百八十九、Cursor 提示词:CRUD
请基于当前 Student 表实现 MyBatis-Plus CRUD。
要求:
1. Entity 使用 @TableName/@TableId
2. Mapper extends BaseMapper<Student>
3. Service extends IService<Student>
4. Impl extends ServiceImpl
5. 请求使用 DTO
6. 返回使用 VO
7. 不直接让 Controller 接收 QueryWrapper
8. 简单查询用 LambdaQueryWrapper
9. 复杂 SQL 保留 XML
10. 后端校验业务字段
三百九十、Cursor 提示词:Wrapper Review
请审查当前项目所有 QueryWrapper/UpdateWrapper。
重点检查:
1. 是否使用字符串字段名,可否改 Lambda
2. 是否把 Wrapper 从 Controller 直接传入
3. 是否有 last/apply/inSql 拼接用户输入
4. and/or 括号逻辑是否正确
5. 是否有空条件导致全表查询
6. UpdateWrapper 是否可能无 WHERE 更新
7. 是否有过度复杂 Wrapper 应改为 XML
三百九十一、Cursor 提示词:分页
请检查 MyBatis-Plus 分页配置。
当前项目是 Spring Boot3 + JDK17。
检查:
1. MP 版本
2. MybatisPlusInterceptor
3. PaginationInnerInterceptor
4. 当前版本是否需要 mybatis-plus-jsqlparser
5. DbType 是否正确
6. 是否同时混用了 PageHelper startPage
7. 是否设置合理 pageSize 上限
8. 自定义 Mapper 分页参数是否正确
三百九十二、Cursor 提示词:逻辑删除
请检查项目逻辑删除设计。
要求:
1. 哪些表真的适合逻辑删除
2. @TableLogic / 全局配置是否统一
3. 删除值和未删除值是否一致
4. 自定义 SQL 是否可能查询到已删除数据
5. 唯一索引是否和逻辑删除冲突
6. 不要机械给所有业务表都加 deleted
三百九十三、Cursor 提示词:乐观锁
请为当前实体增加 MyBatis-Plus 乐观锁。
要求:
1. 数据库增加 version
2. Entity @Version
3. 配置 OptimisticLockerInnerInterceptor
4. 更新必须携带读取到的旧 version
5. 更新 0 行时提示并发修改
6. 不要用乐观锁替代所有状态条件更新
7. 分析当前业务是更适合 @Version 还是 oldStatus 条件
三百九十四、Cursor 提示词:MyBatis + MP 混用
请审查当前项目哪些 Mapper 适合改用 MyBatis-Plus,
哪些应该继续保留 XML。
分类规则:
1. 单表简单 CRUD → MP
2. 简单动态条件 → LambdaQueryWrapper
3. 多表 JOIN → XML
4. 复杂统计 → XML
5. 若依 DataScope SQL → 优先保留 XML
6. 不为了“统一技术”而重写成熟 SQL
输出具体 Mapper 和建议。
三百九十五、最终项目检查表
[ ] Spring Boot3 使用正确 Starter
[ ] MP 版本一致
[ ] MapperScan 正确
[ ] Entity 表名正确
[ ] @TableId 正确
[ ] 主键策略正确
[ ] BaseMapper 正常
[ ] IService/ServiceImpl 正常
[ ] LambdaQueryWrapper 正常
[ ] UpdateWrapper 有安全 WHERE
[ ] Wrapper 不从前端直接传
[ ] 无用户可控 SQL 片段
[ ] 分页插件正常
[ ] 3.5.9+ 检查 jsqlparser
[ ] 逻辑删除策略正确
[ ] 唯一索引兼容逻辑删除
[ ] MetaObjectHandler 生效
[ ] update 自动填充场景验证
[ ] 乐观锁插件生效
[ ] 更新 0 行有处理
[ ] 复杂 JOIN 使用 XML
[ ] DataScope 没被 MP 改坏
[ ] DTO/VO/Entity 边界清晰
[ ] 事务仍由 Spring 管理
[ ] SQL 仍有索引意识
三百九十六、MyBatis-Plus 最重要的 20 条原则
1. MyBatis-Plus 是 MyBatis 增强,不是替代
2. 简单 CRUD 优先 BaseMapper
3. 复杂 SQL 继续写 Mapper XML
4. LambdaQueryWrapper 优先于字符串字段 QueryWrapper
5. Wrapper 是后端 SQL 构造工具,不是前端请求模型
6. 不要让用户控制 last/apply/inSql SQL 片段
7. IService 是工具,不是取消业务 Service
8. Service 仍然负责事务和业务规则
9. Entity 不应该无脑作为 API DTO
10. MP 不会自动解决业务权限
11. MP 不会自动解决 DataScope
12. MP 不会自动解决事务
13. MP 不会自动解决慢 SQL
14. 分页需要正确配置 PaginationInnerInterceptor
15. 3.5.9+ 注意 JSQLParser 可选依赖变化
16. 逻辑删除要同时考虑唯一索引
17. 自动填充要验证不同 update 调用方式
18. 乐观锁更新后必须检查结果
19. 状态条件更新仍然非常有价值
20. 工具越方便,越不能丢掉 SQL 和数据库基础
三百九十七、本章最终验收
学完以后,你应该能独立回答并实现:
1. MyBatis-Plus 是什么
2. 与 MyBatis 什么关系
3. Spring Boot3 如何引入
4. BaseMapper
5. IService
6. ServiceImpl
7. @TableName
8. @TableId
9. @TableField
10. IdType
11. QueryWrapper
12. LambdaQueryWrapper
13. eq / like / in / between
14. 动态 condition
15. and / or
16. LambdaUpdateWrapper
17. 条件更新
18. Page / IPage
19. PaginationInnerInterceptor
20. mybatis-plus-jsqlparser
21. @TableLogic
22. MetaObjectHandler
23. FieldFill
24. @Version
25. OptimisticLockerInnerInterceptor
26. saveBatch
27. selectOne
28. 代码生成器
29. FastAutoGenerator
30. MP 与原生 MyBatis 混用
31. MP 与若依混用
32. MP 与 DataScope
33. MP 与 PageHelper
34. Wrapper 安全
35. DTO / VO / Entity 边界
36. 什么时候该写 XML
三百九十八、第三阶段结束
到这里:
第三阶段 Java 企业项目 + AI 助手
已经完成。
你这一阶段已经学过:
Vue
Activiti
Git
Redis
若依
完整淘车湾项目实战
企业开发流程
MyBatis-Plus
三百九十九、下一阶段
按照课程表,下一阶段正式进入:
第四阶段:微服务项目 + AI 应用
第一课:
《微服务体系架构:系统演化与 AI 介绍》
接下来会从:
单体架构
↓
垂直架构
↓
分布式架构
↓
微服务架构
一步一步讲。
并重点回答:
为什么一个 Spring Boot 项目最后要拆微服务?
什么时候不应该拆?
服务之间怎么调用?
为什么需要注册中心?
Nacos 是什么?
Feign 是什么?
Gateway 为什么存在?
Sentinel 解决什么?
微服务会带来哪些新问题?
CAP、BASE、分布式事务是什么?
淘车湾这种项目为什么当前不需要微服务?
真正的微服务项目是怎么拆的?
这会正式进入:
Spring Cloud Alibaba
的学习阶段。