MyBatis-Plus详解

O泡李华 9

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

的学习阶段。