MyBatis-Plus 核心实战:条件构造器、逻辑删除、分页插件与代码生成器

MyBatis-Plus 核心实战:条件构造器、逻辑删除、分页插件与代码生成器
神经蛙每个 Spring Boot 项目几乎都要写 CRUD,而原生 MyBatis 的痛点是:每个表要手写 Mapper.xml、重复的 resultMap、分页要自己写 count(*) 再拼 LIMIT、逻辑删除靠 WHERE deleted=0 处处粘贴。MyBatis-Plus(下文简称 MP)定位很明确——只做增强,不做改变:你原来的 MyBatis 代码一行都不用动,只是在上面叠了一层开箱即用的能力。本文聚焦 MP 自身,不重复讲 MyBatis 的 SqlSession/动态 SQL 原理,也不碰并发、集合、JVM 那些独立主题。
一、为什么需要 MyBatis-Plus
先看一段原生 MyBatis 最常见的「样板代码」:
1 | // 查询用户列表,按状态过滤 + 分页 |
1 | <select id="selectByStatus" resultType="User"> |
表一多,这种 CRUD 模板铺天盖地。MP 把这些全部收编成通用能力,你只写「不一样」的那部分。
| 痛点 | 原生 MyBatis | MyBatis-Plus |
|---|---|---|
| 单表 CRUD | 手写 Mapper + XML | BaseMapper 内置 17 个方法 |
| 分页 | 手写 count + limit | PaginationInnerInterceptor 自动分页 |
| 逻辑删除 | 处处拼 deleted=0 |
@TableLogic 全局自动过滤 |
| 条件查询 | XML 拼接 or ${} 拼串 |
QueryWrapper 类型安全 |
| 字段填充 | 手动 set | MetaObjectHandler 自动填充 |
| 代码生成 | 无 | FastAutoGenerator 一键生成 |
二、快速集成
引入 starter 即可,注意 MP 的 starter 已经排除了原生 MyBatis,不要再同时引 mybatis-spring-boot-starter,否则版本会打架:
1 | <dependency> |
1 | mybatis-plus: |
启动类扫描 Mapper 接口:
1 |
|
MP 用的是 mybatis-plus-spring-boot3-starter(适配 Spring Boot 3),老项目 Spring Boot 2 用 mybatis-plus-boot-starter。选错 starter 会启动失败。
三、实体与表映射
MP 通过注解把实体和表结构关联起来,无需 XML:
1 | // 表名,默认按类名驼峰转下划线 |
| 注解 | 作用 | 常用取值 |
|---|---|---|
@TableName |
绑定表名 | 字符串表名 |
@TableId |
主键 | AUTO / ASSIGN_ID / INPUT |
@TableField |
字段映射 | value、fill、exist=false |
@TableLogic |
逻辑删除 | 标注在删除标记字段 |
四、通用 Mapper:BaseMapper 内置 CRUD
让 Mapper 继承 BaseMapper<T>,17 个常用方法直接可用:
1 | public interface UserMapper extends BaseMapper<User> { |
1 | User user = userMapper.selectById(1L); |
selectList(null) 的 null 是 QueryWrapper 参数,传 null 表示无条件。这也正是下一节条件构造器的入口。
五、条件构造器(核心)
条件构造器是 MP 最常用也最容易被误用的能力。三种主力:
| 构造器 | 用法 | 推荐度 |
|---|---|---|
QueryWrapper |
用字符串写列名,如 eq("name", "Tom") |
一般 |
LambdaQueryWrapper |
用方法引用写列,如 eq(User::getName, "Tom") |
⭐ 推荐 |
UpdateWrapper |
带更新的 set 条件 | 更新场景 |
推荐 LambdaQueryWrapper,因为编译期就能发现字段拼错,重构安全:
1 | LambdaQueryWrapper<User> w = new LambdaQueryWrapper<>(); |
更新用 LambdaUpdateWrapper(不用先查再 set):
1 | LambdaUpdateWrapper<User> uw = new LambdaUpdateWrapper<>(); |
绝对不要用 QueryWrapper 拼接前端传来的字符串当列名或值,例如 eq(前端字段, 前端值)。列名必须白名单校验,值用 #{}(构造器内部已经是预编译参数),否则有注入风险。构造器生成的 SQL 全部走 ? 预编译,本身是安全的——危险的是你拿外部输入当「列名」。
六、自定义 SQL 与 Wrapper 共存
MP 不排斥你写 XML/注解 SQL,而且能让自定义 SQL 复用 Wrapper 条件:
1 |
|
1 | userMapper.selectCustom( |
${ew.customSqlSegment} 会把 wrapper 编译成 WHERE ...,既保留手写 SQL 的自由,又享受条件构造器的便利。
七、分页插件
MP 的分页是拦截器式的——你只管传 Page,它自动帮你拼 count 和 limit:
1 |
|
1 | Page<User> page = new Page<>(1, 10); // 第 1 页,每页 10 条 |
联表分页(多表 JOIN)selectPage 的 count 会按主表 count,常算错。实践里有两条路:① 用 @Select 自定义 SQL 并自己写 count 方法;② 先分页查出主表 ID,再 IN 查询关联数据(避免深分页 JOIN)。
八、逻辑删除
逻辑删除让 deleteById 变成 UPDATE ... SET deleted=1,查询自动追加 deleted=0,业务无感知:
- 全局配置(见第二节 yaml):
logic-delete-field: deleted后,所有实体有该字段即生效; - 或按字段单独标
@TableLogic覆盖全局。
1 | userMapper.deleteById(1L); |
逻辑删除的三个坑:① 被标记的字段不要建唯一索引(否则两条「已删除」数据会冲突,用「唯一列 + deleted」联合索引缓解);② selectCount 默认也带 deleted=0,统计已删除要手写 SQL;③ 逻辑删除字段不要和「是否禁用」等业务字段混用,语义会乱。
九、自动填充与乐观锁
自动填充:用 MetaObjectHandler 统一处理 create_time / update_time,告别每次手动 set:
1 |
|
乐观锁:高并发更新防覆盖,靠 @Version + 拦截器:
1 |
|
1 | interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); |
更新失败(version 不匹配)返回影响行数 0,业务层据此重试或抛错。
十、代码生成器 FastAutoGenerator
不想手写 Entity/Mapper/Service/Controller?3.x 的 FastAutoGenerator 一条链搞定:
1 | FastAutoGenerator.create("jdbc:mysql://localhost:3306/demo", "root", "pwd") |
执行后直接产出:User.java / UserMapper.java(含 extends BaseMapper<User>)/ UserService + UserServiceImpl / UserController,并自动带 Swagger/RESTful 风格。
十一、与 MyBatis 原生共存 & 插件生态
MP 完全兼容原生 MyBatis:你的老 @Select、XML、<resultMap>、<foreach> 全部照常工作,MP 只是在 MyBatis 生命周期里插入了拦截器。常用内置拦截器还有:
TenantLineInnerInterceptor:多租户,自动拼tenant_id;DynamicTableNameInnerInterceptor:分表时按规则动态替换表名;DataChangeRecorderInnerInterceptor:数据变更记录(审计)。
插件通过 MybatisPlusInterceptor.addInnerInterceptor(...) 顺序注册。分页插件建议放在最后,避免其他插件改写 SQL 后分页拦截器拿不到正确语句。
十二、MP vs MyBatis vs JPA 选型对照
| 维度 | MyBatis(原生) | MyBatis-Plus | Spring Data JPA |
|---|---|---|---|
| 单表 CRUD 效率 | 低(手写) | 高(通用 Mapper) | 高 |
| 复杂 SQL 可控性 | 最高 | 高(XML 共存) | 低(JPQL 受限) |
| 学习曲线 | 中 | 低 | 中 |
| 动态条件查询 | XML 拼 | Wrapper 类型安全 | Specification 繁琐 |
| 数据库移植性 | 强 | 强 | 最强 |
| 适合场景 | 报表/复杂 SQL | 业务后台(CRUD 多) | 领域模型驱动 |
结论:后台管理系统、内部业务系统这种「表多 CRUD 多」的场景,MP 性价比最高;需要大量手写复杂报表 SQL 的,原生 MyBatis 或直接 JDBC 更顺手。
十三、10 条生产避坑清单
- starter 别重复引:引了
mybatis-plus-*-starter就不要再引原生mybatis-spring-boot-starter,版本冲突直接NoClassDefFound。 - 列名用 Lambda 写法:
LambdaQueryWrapper比字符串QueryWrapper多一层编译期校验,重命名字段时 IDE 会提醒。 last()慎用:last("LIMIT 1")是直接字符串拼接,参数不可来自外部输入,否则注入。- 逻辑删除 + 唯一索引:把
deleted纳入联合唯一索引,或避免对该字段建唯一索引。 - 分页拦截器放最后:多插件顺序注册时,分页放末尾,避免 SQL 被改写后分页失效。
- 乐观锁更新要取返回值:
updateById返回 0 说明 version 冲突,必须重试而非静默成功。 selectList(null)会全表扫:生产环境务必带条件或分页,避免一次拉全表。IdType.ASSIGN_ID是 19 位 Long:前端 JS 精度只有 53 位,返回要转String,否则末几位被截断。- 多数据源别只配一个分页:每个数据源都要各自注册
MybatisPlusInterceptor,否则其中一个不分页。 exist=false标注非表字段:实体里的private String keyword(查询条件字段)一定要标@TableField(exist = false),否则 insert 报「未知列」。
MP 的哲学是「增强不替换」:它让 80% 的单表 CRUD 零代码,又把那 20% 复杂 SQL 完整留给你。掌握条件构造器、分页、逻辑删除、自动填充这四件套,再加上代码生成器,一个标准后台的持久层开发效率能翻好几倍。
















