前两天帮组里的新人梳理项目结构,发现大家对 Spring Boot 整合 MyBatis-Plus 这件事普遍处在“能跑通但不懂原理”的阶段。跟着博客敲一遍能出结果,换一个真实业务场景就卡住半天,要么被一个小配置绊倒,要么说不清为什么这么配。我干脆把从零搭工程、整合、写 CRUD,到分页、自动填充、逻辑删除、乐观锁这些高频功能,以及自己实际踩过的坑完整整理了一遍,照着敲就行。
这篇文章默认你懂 Java 基础、会用 Maven 和 IDEA,但不要求你之前用过 MyBatis。代码全部给全,配置逐行说明,图片需要的地方用文字描述清楚。保姆级不是只给结果,重点是告诉你每一步背后的原因,这样换一个项目你也能自己搞定。
1. 为什么要在 Spring Boot 项目里引入 MyBatis-Plus
1.1 它把原生 MyBatis 里最琐碎的重复工作给干掉了
先想一个最普通的场景:一张用户表,你要写新增、按 ID 查询、按 ID 更新、按 ID 删除。用原生 MyBatis,你得在 Mapper.xml 里写四个 SQL,每个 SQL 要处理参数映射、结果集映射,再写一个 Mapper 接口。单表还能忍,表一多,这种重复代码会占掉你大量的开发时间,而且通常一点技术含量都没有。
MyBatis-Plus 的做法是提供一个BaseMapper<T>,你只要让 Mapper 接口继承它,上面说的单表 CRUD 就全部有了。不需要写 XML,不需要写 SQL,insert、deleteById、selectById、updateById 这些方法直接调用。我见过不少第一次用的人很怀疑:真的不用写 SQL 吗?真的不用。MyBatis-Plus 会在运行时根据实体类上的注解自动生成 SQL,比如selectById内部拼出来的就是SELECT id,name,age,email FROM user WHERE id=?。
这样省下来的时间不是一点点。一个项目二三十张表,每张表省四五个 SQL,那就是一百多个不必要维护的模板代码。团队新人有更多精力去写真正的业务逻辑,而不是天天复制粘贴 SQL。
1.2 它和 Spring Data JPA 不是一个赛道,别纠结选谁
很多人在选 ORM 的时候会拿 MyBatis-Plus 和 Spring Data JPA 做对比。JPA 走的是全自动 ORM 路线,实体类关联关系映射得很完善,父类继承、懒加载这些做得非常细;而 MyBatis-Plus 本质上是 MyBatis 的增强工具,没有改变 MyBatis 的 SQL 灵活性,只是在单表操作层面帮你去掉了重复劳动。
项目里到底用哪个,主要看团队写 SQL 的习惯。如果你的团队喜欢把复杂 SQL 牢牢握在自己手里,用 MyBatis-Plus 会舒服很多。它不替你做多表关联,也不会在你的复杂查询里帮倒忙。需要写 join 的时候,老老实实写 XML,完全不受影响。而 JPA 查复杂报表时,往往要借助 Specification 或者 JPQL,学习成本和纠错成本都偏高。
1.3 为什么现在这么多项目都在用
核心原因就是四个字:非侵入式。你的 Mapper 还是那个 Mapper,你的实体类也还是普通 POJO,MyBatis-Plus 只是提供了通用方法和插件,你不用它的时候,项目也可以正常跑。对老项目改造很友好,对新项目又足够快。
加上它把分页、逻辑删除、自动填充、乐观锁这些日常必备功能都做成了插件,程序员不需要自己造轮子。项目要的是按时交付,而不是把时间花在写一个通用分页插件上。MyBatis-Plus 这套东西的使用门槛不高,文档也一直有人维护,社区有大量现成踩坑记录,企业在选型时也会考虑这个因素。
不过也别把它神话。它擅长的是单表 CRUD 和通用能力,真正复杂的业务查询、多表关联分析、大数据量报表,该用原生 SQL 还是要用。正确的心态是:用它来提高效率,而不是以为它能替代你写 SQL 的能力。
2. 从零搭建工程骨架:版本选型比代码先决定成败
2.1 先准备一套能直接复制的环境参数
搭建工程之前,先确定 JDK、Spring Boot、MyBatis-Plus 之间的版本对应关系。很多人项目启动就报错,翻半天发现是 Spring Boot 3 用了 MyBatis-Plus 旧版本,包名javax和jakarta不兼容导致的。
我目前的建议组合如下。
| 场景 | JDK | Spring Boot | MyBatis-Plus | 数据库 |
|---|---|---|---|---|
| 传统稳定方案 | JDK 8 | 2.7.x | 3.5.x 任意新版本 | MySQL 5.7 / 8.0 |
| 新项目推荐 | JDK 17 | 3.2.x / 3.3.x | 3.5.3+ 或 3.5.x 最新版 | MySQL 8.0 |
Spring Boot 3 必须配 JDK 17 以上,同时 MyBatis-Plus 要选择mybatis-plus-spring-boot3-starter,注意这个 starter 从 3.5.3 版本开始才稳定可用。如果你在 Spring Boot 3 项目里用了旧的mybatis-plus-boot-starter,很可能启动直接报 ClassNotFoundException。
数据库驱动也要注意:Spring Boot 2.x 时代默认用 MySQL Connector/J 8.x,Spring Boot 3 里依赖管理也内置了正确版本,不需要手动指定。
2.2 用 Spring Initializr 创建最基础的 Spring Boot 工程
打开 IDEA,新建项目,选择 Spring Initializr。Group 填你公司的域名反写,Artifact 填项目名,Java 版本按前面表格选 17 或 8。
关键点在依赖选择这里。刚开始学习时,只勾选Spring Web和MySQL Driver就够了,其他像 MyBatis、Redis、Security 后面全用 Maven 坐标手动加。为什么要这么保守?因为 IDEA 的依赖选择界面里 MyBatis 相关项并不是 MyBatis-Plus,直接选容易误导入原生 MyBatis。等 Maven 打包过一遍,搞清楚哪些依赖是干什么的,再逐步加东西,排查问题会轻松很多。
创建完之后你会得到标准的目录结构:src/main/java下是代码,src/main/resources下是配置,启动类上一篇文章口吻里的那个main方法入口。先把项目跑起来一次,什么都不写,启动日志显示 Tomcat started 就说明基础环境没问题。
2.3 引入 MyBatis-Plus 依赖的正确姿势
打开pom.xml,在<dependencies>里加下面这段:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency>如果项目是 Spring Boot 3,依赖要换成:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>版本号以 Maven 中央仓库显示的最新稳定版为准,这里只是示例。注意:不要手动再加mybatis和mybatis-spring的依赖,这个 starter 内部已经带好了,自己加很容易出现版本冲突,导致启动时出现Invalid value type for attribute 'factoryBeanObjectType'之类的莫名其妙报错。
2.4 准备数据库和配置文件
在 MySQL 里建一个测试库,表结构尽量贴近真实业务。示例 SQL 如下:
CREATE DATABASE IF NOT EXISTS demo_mp DEFAULT CHARACTER SET utf8mb4; USE demo_mp; CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, create_time DATETIME DEFAULT NULL, update_time DATETIME DEFAULT NULL, deleted TINYINT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;deleted字段先留着,后面讲逻辑删除会用到。然后打开src/main/resources/application.yml,写入:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_mp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: automap-underscore-to-camel-case这一项非常关键。数据库字段习惯用下划线命名,Java 实体类习惯用小驼峰,user_name对应userName,开启这项才能自动映射。MyBatis-Plus 默认是开启的,但建议自己在配置里写出来,因为后期排查问题时你一眼能确认,而不是靠默认值猜。
2.5 启动类上加上 @MapperScan
MyBatis-Plus 需要知道你哪些接口是 Mapper。两种方式:在每个 Mapper 接口上加@Mapper,或者直接在启动类上扫描整个包。我推荐后者:
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }@MapperScan的作用是把指定包下的接口注册到 Spring 容器里,这样 Mapper 才能被注入到 Service。扫描路径要和你的 Mapper 包路径一致,包名不一致是新手最容易犯的错误,启动后找不到 Mapper 的报错基本都是这里的问题。
3. 核心 CRUD 打通:从实体类到 Controller
3.1 实体类注解:每个字段都要有交代
在com.example.demo.entity包下创建User:
package com.example.demo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDateTime; @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic @TableField(value = "deleted") private Integer deleted; // getter/setter 省略,实际代码要写上 }三个核心注解要理解透:
@TableName("user"):实体类对应的表名。如果类名和表名不一样,比如UserEntity对应sys_user,不加这个注解就会报错。我习惯哪怕名字一样也显式写上,代码自解释性更强。@TableId(type = IdType.AUTO):主键策略。数据库主键自增就配 AUTO,如果是雪花算法生成的主键就不需要这个注解,默认分配 ID。AUTO 和 INPUT 的区别后面踩坑部分详细说。@TableField(value = "deleted"):当字段名和表字段不一致时用 value 指定列名,fill = FieldFill.INSERT表示插入时自动填充,后面配合 MetaObjectHandler 使用。
注意实体类的字段类型建议用包装类型Long、Integer,不要用long、int。原因很简单:如果查询结果是 NULL,基本类型拆箱会直接抛NullPointerException。
3.2 Mapper 接口:继承 BaseMapper 就算完成数据访问层
创建com.example.demo.mapper.UserMapper:
package com.example.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; public interface UserMapper extends BaseMapper<User> { }这样你不需要写一行 SQL,就已经拥有了insert、deleteById、updateById、selectById、selectList、selectCount等常用方法。
BaseMapper 内置方法里几个高频率的:
| 方法 | 作用 |
|---|---|
selectById | 按主键查询单条 |
selectList(Wrapper) | 按条件查询列表 |
selectOne(Wrapper) | 按条件查询单条,多条会报错 |
insert | 插入一条记录 |
updateById | 按主键更新,null 字段不更新 |
deleteById | 按主键删除 |
Wrapper是 MyBatis-Plus 的条件构造器,后面在 Controller 里演示。在真实项目中,我知道很多人喜欢直接在 Controller 注入 Mapper 用,但这会把你带到坑里去。正确做法是 Service 依赖 Mapper,Controller 依赖 Service,保持分层清晰。
3.3 Service 层:用 IService 减少大量样板代码
新建UserService:
package com.example.demo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.entity.User; public interface UserService extends IService<User> { }再建实现类UserServiceImpl:
package com.example.demo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }是不是很抽象?为什么接口和实现类都是空的?因为IService和ServiceImpl已经把save、getById、list、updateById、removeById、lambdaQuery、lambdaUpdate这些方法全部实现了,你要做的只是指定实体类型和 Mapper。
这么做的最大价值是:Controller 调用userService.save(user)时,走的是 Service 层事务边界;哪天你想在保存用户后加一条短信通知逻辑,直接在 ServiceImpl 里重写save方法覆盖默认行为即可,不用去改 Controller 和 Mapper。这就是分层带来的扩展性。
3.4 Controller:写一个能直接测试的 CRUD 接口
package com.example.demo.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping public User save(@RequestBody User user) { userService.save(user); return user; } @GetMapping("/{id}") public User getById(@PathVariable Long id) { return userService.getById(id); } @GetMapping public List<User> list(@RequestParam(required = false) String name) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); if (name != null && !name.isBlank()) { wrapper.like(User::getName, name); } return userService.list(wrapper); } @PutMapping public boolean update(@RequestBody User user) { return userService.updateById(user); } @DeleteMapping("/{id}") public boolean delete(@PathVariable Long id) { return userService.removeById(id); } }Controller 里演示了LambdaQueryWrapper。这个类是 MyBatis-Plus 最常用的条件构造器,用方法引用User::getName指定字段,编译期就能发现字段名写错的问题,比字符串写法安全得多。这里的wrapper.like(User::getName, name)最终会变成 SQL:WHERE name LIKE '%值%'。
测试一下:启动项目,用 IDEA 自带的 HTTP Client 或 Postman 发起请求。
POST 请求体:
{ "name": "张三", "age": 25, "email": "zhangsan@example.com" }返回数据里会有生成的 ID 和自动填充的时间字段(自动填充在下一章配置)。到这里,工程搭建和 CRUD 就算完整跑通了。
4. 高频功能:分页、自动填充、逻辑删除、乐观锁一次配齐
4.1 分页查询:只加 limit 思路在 MyBatis-Plus 里行不通
直接在代码里拼LIMIT offset, size是可以分页,但真正的业务系统通常要求分页查询同时返回"当前页数据 + 总条数",而且 SQL 里如果带着参数偏移量,写起来又丑又容易漏条件。MyBatis-Plus 的方式是配置一个分页插件,然后调用分页方法。
在新版本(3.4.0 之后)必须自定义MybatisPlusInterceptor,老版本那个PaginationInterceptor已经废弃了。新建配置类:
package com.example.demo.config; 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 MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }DbType.MYSQL一定要设置,分页插件要依据不同数据库语法拼 SQL,MySQL 是LIMIT,PostgreSQL 是LIMIT OFFSET,SQL Server 是OFFSET FETCH NEXT。setMaxLimit(500L)是防止有人恶意传超大页码,超过 500 条自动不给查。
Controller 里分页:
@GetMapping("/page") public Page<User> page(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size) { Page<User> page = new Page<>(current, size); return userService.page(page); }返回的Page对象里自带records、total、current、size这些字段。前端做分页组件时,拿这个结构直接渲染即可。
注意:如果你发现调用page方法后返回的 total 是 0 或者数据是全量查询,先别查 SQL,检查MybatisPlusInterceptor有没有被 Spring 容器管理。这个配置类最常见的坑是没有加@Configuration或者被 ComponentScan 排除外了。
4.2 create_time / update_time 自动填充:业务代码里不再手动 set
很多表都有create_time、update_time字段。如果没有自动填充,每次新增和修改都要写user.setCreateTime(LocalDateTime.now()),漏一次就会为根因排查浪费一小时。MyBatis-Plus 提供了字段自动填充机制。
实体类里已经给字段加了注解:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;再写一个处理器实现MetaObjectHandler:
package com.example.demo.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @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()); } }strictInsertFill和strictUpdateFill是推荐写法,因为它在字段已经有值的时候不会覆盖。比如你在新增前手动setCreateTime了一个特殊时间,自动填充就不会抢走它。
这个功能上线后,你会发现所有新增修改的代码瘦身很明显,原子性的时间和数据库操作在一起,不会出现某个请求忘了记录创建时间的尴尬。
4.3 逻辑删除:数据别真删,除非你想被业务同学追着要数据
现在的业务系统里,物理删除是高风险操作。用户删错了,数据是找不回来的。逻辑删除的思路是在表里加一个标记位(比如deleted字段),删除时 UPDATE 这个标记,查询时自动过滤掉标记过的数据。
实体类上已经写了:
@TableLogic @TableField("deleted") private Integer deleted;再配合全局配置,让 MyBatis-Plus 知道逻辑删除的约定规则。
在application.yml中补充:
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0来一组实际效果:
| 操作 | 实际执行的 SQL |
|---|---|
removeById(1) | UPDATE user SET deleted=1 WHERE id=1 AND deleted=0 |
getById(1) | SELECT ... FROM user WHERE id=1 AND deleted=0 |
selectList(...) | SELECT ... FROM user WHERE ... AND deleted=0 |
也就是说,你什么都不用改,删除操作自动从 DELETE 变成 UPDATE,查询自动带上deleted=0条件。这是 MyBatis-Plus 非常漂亮的设计:对业务代码无侵入,对旧数据兼容性好。
注意@TableLogic只能配一个字段,且逻辑删除字段在实体类里的类型基本用Integer存 0 和 1。假如你有个复杂查询要查被删除的数据,那就只能写原生 SQL,因为绕过 MyBatis-Plus 的方法才不受逻辑删除拦截。
4.4 乐观锁:解决并发更新最后写入覆盖的问题
举一个简单场景:两个人同时打开一个订单页面,A 把价格改成 100 并保存,B 紧接着把价格改成 80 并保存。如果没有任何锁机制,B 的操作直接覆盖了 A,A 认为的数据被偷偷改了。悲观锁用数据库for update解决,但并发量高时性能受影响。乐观锁的思路是:更新前检查数据版本号是否还和当初读到的一致。
MyBatis-Plus 实现乐观锁分三步。
第一步,表加 version 字段:
ALTER TABLE user ADD COLUMN version INT NOT NULL DEFAULT 0;第二步,实体类加字段和@Version注解:
@Version private Integer version;第三步,配置类里加入乐观锁插件:
pagination.addInnerInterceptor(new OptimisticLockerInnerInterceptor());注意OptimisticLockerInnerInterceptor也要加到MybatisPlusInterceptor中。完整代码:
public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }之后用userService.updateById(user)时,只要 user 对象里带了 version 字段,MyBatis-Plus 会自动在 UPDATE 语句里加上WHERE version=?,并把version=version+1一起更新。如果更新影响行数为 0,说明数据被别人改过了,你的更新不会生效,业务层就有机会提醒用户"数据已变更,请刷新后重试"。
乐观锁适合读多写少的场景,比如库存、订单版本、配置项更新。写并发特别高的场景还是要配合其他方案综合评估。
5. 实际项目里踩过的坑:每条都让人卡过半小时以上
5.1 主键策略选错导致插入失败
我接手过一个项目,数据库主键是自增,但实体类没有加@TableId(type = IdType.AUTO)。MyBatis-Plus 默认的IdType在没配置时是在程序里生成雪花 ID,结果每次 insert 都会把雪花 ID 塞给主键列,数据库自增设置完全失效。数据量大了之后主键顺序乱成一团,分页排序逻辑也受牵连。
正确判断标准:如果是 MySQL 自增主键,实体字段指定@TableId(type = IdType.AUTO);如果分库分表或者分布式环境,才用默认雪花 ID。如果主键是业务系统里自己传入的字符串,要用IdType.INPUT。
5.2 updateById 不会更新 null 字段,这是最容易被误解的行为
使用updateById(user)时,传进来的 user 对象某个字段是 null,MyBatis-Plus 默认不会把这个字段更新到 NULL,而是直接跳过。这通常是我们想要的:更新部门时不需要把不传的字段置空。
但有些业务就是要清空某个字段,比如用户取消备注,期望把remark更新为 NULL。这时就得用LambdaUpdateWrapper显式设置:
userService.lambdaUpdate() .eq(User::getId, 1L) .set(User::getRemark, null) .update();如果你希望整个项目更新时允许 null 字段覆盖数据库数据,可以把实体字段注解改成@TableField(updateStrategy = FieldStrategy.IGNORED),但我不建议全局这么干,危险性太大,一个不小心就把别人的数据清空了。
5.3 分页插件不生效:查出来还是全部数据
这是分页里最经典的坑。表象是前端传了 page=1、size=10,接口返回 100 条。排查链路照这个顺序走:
- 先打印 SQL 日志,看打印出来的 SQL 末尾有没有
LIMIT。没有 LIMIT,说明分页插件根本没拦截。 - 看配置类是否注册了
MybatisPlusInterceptorBean,配置类有没有被 Spring 扫描到。最容易犯的是把配置类写在启动类子包之外。 - 确认拦截器顺序上没有早退。在
MybatisPlusInterceptor里,分页插件和乐观锁插件顺序无所谓,但不要使用多个MybatisPlusInterceptorBean。 - Spring Boot 3 项目里用旧包名
com.baomidou.mybatisplus.extension.plugins的老式配置已经失效,必须用inner.Interceptor。
5.4 字段映射不上,报"Unknown column"错误
数据库字段是user_name,实体字段是userName。虽然 MyBatis-Plus 默认开启驼峰映射,但如果你在配置里手抖覆盖了map-underscore-to-camel-case: false,或表字段里有非常规命名,就会出现查询报Unknown column 'username'这种问题。
排查技巧:先看控制台打印的 SQL 语句,拿 SQL 去数据库客户端执行,观察是 SELECT 部分字段名拼错了,还是 WHERE 条件字段拼错了。解决方式是给实体字段加@TableField("user_name")显式指定列名,不要依赖全局映射。毕竟全局配置只管驼峰,管不了所有命名怪癖。
5.5 数据库字段用了保留字或特殊字符
我给一张业务表加字段时,顺手把字段命名成了order。在 MySQL 里order是排序关键字,生成的 SQL 直接语法报错。这时候用@TableField明确指定列名并加反引号:
@TableField("`order`") private String orderNo;类似的还有desc、group、rank。写实体类之前,先确认字段名不和数据库保留字冲突,省得后面所有 SQL 都带着反引号,非常难受。
5.6 不要重复引入 mybatis 相关依赖
MyBatis-Plus 的 starter 内部已经引入了mybatis和mybatis-spring。有些同学为了保险,又手动加了一份,结果版本冲突,项目启动时报出各种奇怪的 bean 加载失败。记住一个原则:整合 MyBatis-Plus 时,pom 里只保留一个 MyBatis-Plus 依赖,其他mybatis相关依赖全部不要手动添加,除非你很清楚自己在干什么。
排查依赖冲突可以用 IDEA 自带的 Maven 插件,执行dependency:tree,看到同一个构件出现多个版本就是问题信号。
5.7 自动填充失效:时间字段一直是 null
自动填充失效最常见的原因有三个:注解fill没写、MetaObjectHandler没被 Spring 管理、字段名和实体字段名不一致。特别是第三个,我在代码生成器里发现生成出来的实体字段createTime,但MetaObjectHandler里写成了create_time,strictInsertFill找不到目标字段就直接跳过了。自动填充的字段名必须以实体字段名为准,不是数据库列名。
所以说,新手阶段遇到问题,第一反应千万别是"重装依赖""换框架版本",而是先开 SQL 日志看实际语句。SQL 会告诉你它到底执行了什么,很多看似玄学的问题本质都是配置没生效,而配置没生效的根因往往就在被忽略的基础知识点上。
最后再补充一个我自己的使用习惯。公司里的新项目我一般会配合 MyBatis-Plus 的代码生成器一起用,把实体类、Mapper、Service、Controller 一次性生成出来,生成后手工只改业务相关部分。但代码生成器默认生成的实体类里,@TableField注解经常带着FieldFill的旧写法,所以生成完一定要对着我这篇文章的配置检查一遍,特别是createTime、updateTime、deleted、version这四个字段。养成这个习惯后,基本不会在初始化阶段浪费时间去调那些早就被踩烂的配置问题。