
刚接触 Spring Cloud 微服务的人第一天就被 MyBatis-Plus 的通用 CRUD 惊到这很正常。我在带项目时也经常把 MyBatis-Plus 当作“Java 后端第一个生产力工具”推荐给新人。它不改变 MyBatis 原有的 SQL 执行逻辑却能把手写 Mapper 的重复工作干掉一大半。尤其是进入微服务阶段服务拆多之后每个服务里都有一堆单表增删改查没有这个工具会非常痛苦。这篇文章基本就是我跟着黑马 SpringCloud 微服务课程打基础时把 day1 关于 MyBatis-Plus 的内容做的一次系统整理。我会从“为什么要学它”开始然后是依赖怎么加、配置怎么写、注解怎么用最后把实际开发里最容易踩的坑拉出来遛一遍。内容尽量口语化不整虚的适合刚从 SSM 转过来的同学也适合想快速捡起 MP 的普通 Java 开发。1. 为什么微服务课的第一天要讲 MyBatis-Plus1.1 day1 场景通用 CRUD 服务是微服务的刚需Spring Cloud 课程一般先从单体工程拆微服务讲起拆完之后你会发现一个现象每个微服务里都有那么几张表要做增删改查而且这些单表操作长得几乎一摸一样。如果继续用传统 MyBatis你就得为每张表写一个 Mapper 接口、一个 XML 文件、一堆 resultMap再为每个实体写一套 insert、update、selectById、deleteById。写第一遍觉得无所谓写多了真的会怀疑人生。MyBatis-Plus 解决的恰恰是这个痛点。它把单表 CRUD 封装成了 BaseMapper 里的现成方法你只要声明一个接口继承它就自动拥有 selectById、selectList、insert、updateById、deleteById 这一整套能力。在微服务课程的第一天引入它本质上就是让你先拥有一个“无状态增删改查”的通用底座后面课程去搞服务拆分、远程调用、配置中心的时候才不会被重复的 DAO 代码拖后腿。我自己的体会是这个顺序安排得很聪明。你要是没先用过 MP后面写网关、订单、用户服务的时候每个服务里都是一堆样板代码课还没上完人就先烦了。先把通用 CRUD 底座搭起来后面所有服务都能用自己的 Mapper 快速跑通业务学习效率完全不一样。1.2 MyBatis-Plus 和 MyBatis 到底是什么关系很多人一开始以为 MyBatis-Plus 是一个新 ORM 框架要重新学一套理论。其实不是。MyBatis-Plus 是 MyBatis 的增强工具它没有替代 MyBatis而是建立在 MyBatis 之上只做增强不做修改。这么说吧MyBatis 是“手写 SQL 的半自动框架”MyBatis-Plus 是“单表操作全自动、复杂查询你继续手写 SQL”的增强版。所以你在项目里既能看到 MP 自带的方法也能继续写自定义 XML SQL。两者完全不冲突。MP 的查询构造器 QueryWrapper、LambdaQueryWrapper 只是帮你生成 SQL 条件最终落到数据库的还是标准 SQL。换句话说你之前学的 MyBatis 的 everything 在这里都还能用只是多了一把更省事的工具。这也解释了为什么课程 day1 敢直接讲 MP。它不是替代你的旧知识而是把旧知识压缩成更高效的新姿势。你不需要焦虑“我 MyBatis 不熟会不会学不会 MP”只要你能理解实体类对应表、字段对应列、Mapper 对应 DAO 这三层关系MP 的入门基本没有门槛。1.3 适合谁看、学完能获得什么这篇内容的受众很明确正在跟着微服务课程学习、需要快速上手 MyBatis-Plus 的人以及工作中被重复 CRUD 折磨、想提升开发效率的 Java 后端开发。如果你是彻底没用过 MyBatis 的小白建议先花半小时补一下 JDBC 和 MyBatis 的基础概念再来读这篇会更顺。学完这篇你会得到这些东西一个可以直接跑起来的 MyBatis-Plus 最小工程常用注解的准确用法和背后的设计原因分页、乐观锁、逻辑删除、自动填充这类生产级配置的写成标准以及几种实战里非常容易翻车的避坑姿势。最后那部分尤其关键。网上教程大多只讲“验证码能跑”不讲“生产环境哪里会炸”。我在实际项目里踩过的坑今天一并整理出来。下面直接进干货。2. 快速跑通一个基础 Mapper依赖、配置与首个 CRUD2.1 初始化项目的三个必要步骤我用一个最简单的 Spring Boot 工程来演示。不管你用 IDEA 还是其他工具创建工程的时候只要勾选 Spring Web 就够了MyBatis-Plus 的依赖我们手动加这样更清楚。第一步在 pom.xml 里引入 MyBatis-Plus 的 starterdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency注意这个包里已经带了 MyBatis 和 MyBatis-Spring 的依赖所以你不必再单独引入 mybatis 和 mybatis-spring。很多人第一次照着老教程配置又重复加了一遍版本一冲突项目直接起不来。第二步配置数据源。在 application.yml 里写spring: datasource: url: jdbc:mysql://localhost:3306/mp_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这里千万注意 url 末尾的 serverTimezone 参数。如果你不开新版 MySQL 驱动会报时区错误这是新手最常见的启动失败原因之一。第三步写一个启动类能扫描到的 Mapper。MP 支持两种方式一种是在 Mapper 接口上加 Mapper 注解另一种是在启动类上加 MapperScan(你的包路径)。我更推荐 MapperScan一次扫描全搞定不用每个接口都加注解。2.2 Spring Boot 版本与 MP 版本搭配的坑版本搭配是这门课第一天最容易翻车的地方。我见过太多人在 Spring Boot 3.x 项目里引入 mybatis-plus-boot-starter结果启动直接报错。原因是 MP 3.5.x 系列的老 starter 是基于 Spring Boot 2 的自动装配逻辑写的放到 Spring Boot 3 上就会因为包路径变化而失效。如果你用的是 Spring Boot 2.5、2.6、2.7 这类版本用我上面写的 com.baomidou:mybatis-plus-boot-starter 没问题。但如果你用的是 Spring Boot 3.x就得引入独立的 starterdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.3.1/version /dependency这个坑一定要记住。它不会在你的 pom 里报错而是会在运行期给你来一下“Invalid value type”之类的诡异异常排查起来非常痛苦。我的建议是建项目前先确认 Spring Boot 大版本再去 MP 官方仓库查对应 starter 名称这一步能帮你省掉一小时。同样需要注意的是 JDK 版本。MP 3.5.x 对 JDK 8 和 JDK 17 都支持得很好但如果你一边用 JDK 21、一边用很老的 MP 版本也可能遇到编译问题。一般跟着课程走Spring Boot 2.7 JDK 8 是最稳的组合新手先别追求最新版本稳定比什么都重要。2.3 第一个实体类和 BaseMapper依赖和配置就绪以后我们来写一个最简单的用户表实体。假设表结构是这样的CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50), age INT, deleted TINYINT DEFAULT 0 );实体类这样写TableName(tb_user) public class User { TableId(type IdType.AUTO) private Long id; TableField(user_name) private String userName; private Integer age; TableLogic private Integer deleted; }然后定义一个 Mapper 接口Mapper public interface UserMapper extends BaseMapperUser { }你没看错这就够了。现在你的 UserMapper 已经拥有了单表 CRUD 的全部能力。执行这样一段测试代码SpringBootTest class UserMapperTest { Autowired private UserMapper userMapper; Test void testCrud() { User user new User(); user.setUserName(张三); user.setAge(20); userMapper.insert(user); User queried userMapper.selectById(user.getId()); System.out.println(queried.getUserName()); user.setAge(21); userMapper.updateById(user); userMapper.deleteById(user.getId()); } }这段代码跑通你的 MP 环境就算彻底落地了。注意了这里我用了 TableName、TableId、TableField 和 TableLogic 四个注解每个注解背后都有一套设计逻辑下面单独用一整章来拆。2.4 运行时要打开的两个开关很多新手跑完上面的代码没有任何输出以为 insert 失败了。其实数据已经插进去了只是控制台没打 SQL。建议在学习阶段打开两样东西MP 的 SQL 日志和 MyBatis 的驼峰映射配置。mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: truelog-impl 会把每个 CRUD 操作生成的真实 SQL 打到控制台。打开它你能看到 MP 是怎么把实体类转成 SQL 的这个过程对理解框架非常有帮助。比如你调用 selectById(1L)日志里会打印“SELECT id,user_name,age FROM tb_user WHERE id?”看到这些就不觉得框架神秘了。map-underscore-to-camel-case 是让 MyBatis 自动把数据库的下划线字段 user_name 映射成 Java 的驼峰属性 userName。MP 默认是开启的但如果你自己写 XML SQL这个配置对 resultType 自动映射同样生效。我的建议是哪怕你已经用了 TableField 显式指定映射这个开关也要保持打开它会帮你少写很多 resultMap。3. 常用注解逐个拆表名、主键、字段、逻辑删除3.1 TableName 不只是换个表名TableName 的直观作用是声明实体类对应哪张数据库表。它最基础的使用场景是当数据库表名和类名不一致时手动指定表名。在我们上面的例子中类名是 User表名是 tb_user所以必须加 TableName(tb_user)。但这个注解还有更省力的用法。如果你的系统里所有表都有统一前缀比如 tb_、t_、sys_你可以不在每个实体上写 TableName而是通过全局配置指定前缀mybatis-plus: global-config: db-config: table-prefix: tb_配置之后User 类自动对应 tb_user 表Order 类自动对应 tb_order 表。只有那些不符合统一命名规则的表才需要单独用 TableName 覆盖。这种全局策略在微服务项目里尤其实用因为服务多了以后每个库的表命名风格通常是一致的配置一次省心很久。还要注意 TableName 里有个 schema 属性用来指定数据库名。如果你在 SQL 里习惯写“库名.表名”或者一个服务连接了多个库可以这样用TableName(value tb_user, schema user_db)。不过大多数微服务项目都是一个服务对应一个库这个属性用得不多知道就行。3.2 TableId 和主键策略每个表都有主键MP 怎么识别主键默认情况下MP 会把名为 id 的字段当主键。如果你的主键不叫 id或者有复合逻辑就必须在主键字段上标注 TableId。TableId 里最核心的是 type 属性也就是主键生成策略。课程里最常用的几种我列出来策略说明适用场景IdType.AUTO数据库自增单库单表最朴素IdType.ASSIGN_ID雪花算法生成分布式ID微服务拆分后的通用方案IdType.ASSIGN_UUIDUUID字符串不太建议性能差、索引碎片大IdType.INPUT用户自己输入业务指定主键的场合ASSIGN_ID 是微服务课程里的重点因为一个订单服务、用户服务拆出去之后数据库自增 ID 在跨库合并、分表分库时会撞车。雪花算法生成的 ID 全局唯一、趋势递增正好适合微服务之间传递主键。这个策略下主键类型建议用 Long因为雪花 ID 是一个 64 位长整型你写成 Integer 会直接溢出报错这是新手最容易踩的坑之一。还需要注意如果你使用数据库自增也就是 IdType.AUTO那么在 insert 之后MP 会自动把数据库生成的主键值回填到实体对象的 id 字段里。这意味着你插完数据后可以直接用 user.getId()不需要再查一次库。我见过一些同事不知道这个特性插完又 select 一次完全没必要。3.3 TableField 的常见应用场景TableField 是实体类里出现频率最高的注解因为它处理的是“Java 属性”和“数据库字段”之间的映射问题。最基础的用法是显式指定数据库字段名TableField(user_name) private String userName;这行代码的含义是Java 里的 userName 属性对应数据库的 user_name 列。如果你开着驼峰映射不写也能正常工作但写上有一个好处——代码即文档看实体类就知道它对应表里哪个字段不用再去翻建表语句。第二个场景是标注非表字段。如果一个属性只是逻辑上存在比如页面传输用的 confirmPassword或者根据其他字段计算出来的 totalPrice它在表里没有对应列。如果不加处理MP 在生成 SQL 时会带上这个字段结果就是 SQL 报错“Unknown column”。解决办法是TableField(exist false) private String confirmPassword;这个坑我在带新人的时候几乎每次都会遇到。凡是实体类里出现“表里不存在”的属性一律加 existfalse否则 insert 和 select 都会崩。第三个场景是配合自动填充。比如 create_time、update_time 这类字段你希望插入时自动填当前时间更新时自动更新。可以把字段标成TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;然后在配置类里实现 MetaObjectHandler 接口把填充逻辑集中写在一处。下面第 4 部分会给出完整代码。3.4 TableLogic 和 Version两个“要配插件才生效”的注解TableLogic 是逻辑删除注解。它的意思是不给数据库执行真正的 DELETE而是把某个标记字段改为已删除值查询时自动过滤掉这些记录。比如我们的实体里有个 deleted 字段加上 TableLogic 后调用 deleteById 实际执行的是UPDATE tb_user SET deleted1 WHERE id? AND deleted0查询时也会自动带上 AND deleted0。这个机制能防止数据误删也更方便做审计。在微服务课程里用户数据、订单数据这类敏感数据强烈建议用逻辑删除。注意TableLogic 默认约定 deleted1 表示已删除deleted0 表示未删除。如果你的系统标记值不一样可以在注解里覆盖也可以在 yml 里全局配置 logic-delete-value、logic-not-delete-value。逻辑删除字段必须放到实体里不能只存在于数据库表。Version 是乐观锁注解。用在版本号字段上比如 order_version。用法是Version private Integer version;配上乐观锁插件后MP 执行 updateById 时会在 UPDATE 语句里自动拼接“SET versionversion1 WHERE version旧值”如果旧值不匹配更新影响行数为 0说明期间有人改过数据你可以再做重试或报错处理。这个机制在微服务架构里特别有用能防止多个服务同时操作同一条数据造成的覆盖丢失。但这里有一个很容易被忽视的点TableLogic 和 Version 本身只是“标记”真正让它们生效的是对应的 MybatisPlusInterceptor 插件。如果你只加注解不加插件逻辑删除会被当成普通字段输出乐观锁也不会生效。很多人代码看着没问题行为却不正常多半是插件没注册。4. 常用配置与插件整理生产可用必须补的三块4.1 分页插件为什么必须单独配MP 的 selectPage 方法如果没有分页插件支持你会发现它查出来的数据其实是全量数据只是外表包了一层分页对象。这是很多人“分页不生效”的根源。分页不是 MP 内置默认功能必须通过插件在 SQL 执行前做拦截改写往 SQL 尾部拼 LIMIT。推荐的分页插件配置长这样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你如果是 PostgreSQL 或 Oracle 就必须改成对应枚举否则 SQL 语法不对。maxLimit 是单次查询的最大限制防止有人一次性拉全表生产环境建议一定要设。写分页查询的代码也非常简单PageUser page userMapper.selectPage( new Page(1, 10), new LambdaQueryWrapperUser().eq(User::getAge, 20) );返回的 Page 对象里有 records、total、current、size 这几个字段前端做分页展示时直接把这些字段透传过去就行。课程里喜欢把它封装成统一分页返回对象这是好习惯但刚开始不用搞那么复杂先跑通原生 Page 再说。4.2 乐观锁插件和自动填充的完整代码前面讲了 Version 需要配套插件这里给出完整配置。在刚才的 MybatisPlusConfig 里继续注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } Bean public MetaObjectHandler metaObjectHandler() { return new 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()); } }; } }MetaObjectHandler 是自动填充的核心接口。当实体字段标注了 TableField(fill FieldFill.INSERT) 时MP 在 insert 前会调用 insertFill 方法把当前时间填进去。update 时则只更新标了 INSERT_UPDATE 的字段。这里有一个非常隐蔽的坑strictInsertFill 只有在字段值为 null 的时候才会填充。如果你的实体里 setCreateTime 了一个非 null 值不管 handler 写得多正确MP 都只会用你 set 的值。这在业务上其实是合理的——允许调用方手动指定时间。但反过来如果你希望强制统一时间就得把 strict 版本换成 fillStrategy 并自己判断。自动填充字段在微服务课程里非常常见create_time、update_time 基本每张表都有。统一用 handler 处理代码里就不需要每个服务都手写时间赋值了整洁度提升一大截。4.3 yml 全局配置清单一次看懂直接复制很多教程把 MP 配置散落在各个代码片段里看起来零碎。我整理了一份我自己项目里常用的 yml 配置你拿过去可以根据实际情况删减mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: id-type: assign_id table-prefix: tb_ logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml逐项解释一下map-underscore-to-camel-case开启驼峰映射大部分项目都为 true。log-impl开发环境打印 SQL生产环境务必去掉。banner控制台那个大头文字 logo关闭后输出干净一点。id-type全局主键策略。如果你大部分表都用雪花 ID设置 assign_id 可以少写很多 TableId。logic-delete-field全局逻辑删除字段。配置后所有实体里的 deleted 字段都会被自动识别为逻辑删除不必每个实体都加 TableLogic。mapper-locations自定义 XML SQL 文件的扫描路径。如果你不用 XML这个配置可以省略。但一旦有自定义 mapper 就需要它否则 MP 会报找不到 SQL 的错误。这里要提醒一句全局配置和注解是“互补”关系不是“覆盖”关系。比如你全局设了 id-type: assign_id但某个实体特意用 TableId(type IdType.AUTO) 覆盖注解优先级更高。理解这个优先级顺序后面排查问题时思路会清晰很多。4.4 代码生成器要不要用如果课程进度比较快老师可能会提到 MyBatis-Plus 的代码生成器。它可以根据数据库表结构自动生成实体类、Mapper、Service、Controller是很多人印象里的“一键建全套”。我的观点是可以学但别依赖。代码生成器适合那种表特别多、字段特别标准、后期不咋改的模块。比如你一下子要做 20 张表的字典管理模块用生成器能省大量体力。但微服务学习阶段实体类的字段命名、注解选择本身就是你理解框架的重要环节如果全部自动生成你会错过很多细节。而且生成器生成的代码有两个问题一是不一定符合你项目的统一风格二是一旦表结构变了重新生成会覆盖你自己的业务修改。我自己的实践是小项目手写实体和 Mapper大项目同步用生成器做一个初版然后人工调整字段注解和逻辑删除配置。这样效率和可控性都有保障。5. 踩坑实录从字段映射到批量插入5.1 驼峰映射失效可能是你自己写了 XML这个坑我在真实项目里踩过印象极深。现象是用 MP 的 BaseMapper 方法时一切正常但一执行自定义 XML SQL查询结果里某些字段就是 null。排查下来原因是自定义 SQL 返回的类型没有用 resultMap而是用了 resultType并且 SQL 里没有给下划线字段起驼峰别名。比如你的 SQL 是SELECT id, user_name, age FROM tb_user WHERE id #{id}如果 resultType 指向 UserMyBatis 自动映射 user_name 列到 userName 属性时依赖的是 map-underscore-to-camel-case 配置。这个配置在 MP 全局配置里开着但如果你在 Spring Boot 里同时存在自定义的 MyBatis 配置某些情况下这个开关可能没生效。稳妥做法是自定义 SQL 一律写 resultMap或者在 SQL 里显式别名SELECT id, user_name AS userName, age FROM tb_user WHERE id #{id}我个人更推荐写 resultMap因为在复杂的多表联查里别名方式会让 SQL 变得很乱。与其在 SQL 里绕不如直接定义一个清晰的 resultMap这才是 MyBatis 的正规做法。5.2 雪花 ID 用 Long 还是 String前后端协作的大坑课程里用 ASSIGN_ID 生成雪花 ID 时后端实体主键一般用 Long。但在做 Web 接口时Long 类型在 JavaScript 里可能会丢失精度。JavaScript 的 Number 安全整数范围只有 2 的 53 次方左右而雪花 ID 是 64 位长整型前端的 JS 接收到之后超过安全范围的数字会出现末尾几位变成 0 的情况。这不是 MP 的问题而是前后端数据类型精度问题。解决方案主要有三种第一种实体类里主键用 Long但在给前端返回的 VO/DTO 里把 ID 字段改成 String序列化时输出字符串。 第二种在主键字段上加注解让 Jackson 在序列化时把 Long 转成 StringJsonSerialize(using ToStringSerializer.class) private Long id;第三种在 Spring Boot 全局配置 Jackson 的 Long 转 String 策略但这种做法影响面太大所有 Long 都会变成字符串我不太建议。我推荐第一种或第二种。微服务之间内部调用走 RPC 时ID 类型保持 Long 即可对外提供 HTTP 接口时把 ID 转成字符串这个细节让前后端联调少开很多次会。5.3 saveBatch 不是万能快需要开启 JDBC 批量参数MP 的 IService 接口里有个 saveBatch 方法很多人以为它是一个真正的批量插入性能一定很高。其实要分情况。默认情况下saveBatch 是把一批数据循环执行 insert利用的是同一个数据库连接的 Statement 缓存并不会自动生成那种“一次插入多行 VALUES”的 SQL。想让批量插入真正快起来得给 JDBC URL 加上一个参数spring: datasource: url: jdbc:mysql://localhost:3306/mp_demo?rewriteBatchedStatementstrue打开 rewriteBatchedStatements 后MySQL 驱动会把强制的一次一次 insert 重写为一个批量 INSERT 语句性能提升非常明显。我在一个导入 Excel 的场景里做过对比一万条数据没开参数用时 8 秒开了以后不到 2 秒。另外要注意saveBatch 用的 SQL 默认包含所有字段如果你要插入的字段特别多单条 SQL 会很长数据库参数限制可能触发异常。这种情况下建议用 LambdaUpdateWrapper 按需构造或者在实体上使用 TableField(insertStrategy FieldStrategy.NOT_NULL) 来控制插入字段。这里面的门道比较多先记住两个核心点要开 rewriteBatchedStatements要控制单批大小500 到 1000 条一批是比较稳妥的经验值。5.4 常见问题速查表我把这段时间遇到的高频问题整理成了一张表方便你遇到问题时快速对照。现象可能原因解决办法启动报时区错误JDBC URL 没有 serverTimezone 参数改成 Asia/Shanghai主键插不进去或类型错误IdType 与数据库生成策略不一致检查 TableId 和物理表主键删除操作没有真正删除数据实体里加了 TableLogic确认这是否是你想要的逻辑删除分页返回总条数不正确分页插件未注册检查 MybatisPlusInterceptor乐观锁更新不生效未注册乐观锁插件添加 OptimisticLockerInnerInterceptor属性名带下划线查不到值驼峰映射配置没生效开启 map-underscore-to-camel-caseSQL 报 unknown column实体里存在非表字段加 TableField(exist false)Spring Boot 3 启动异常starter 选错用 mybatis-plus-spring-boot3-starter自动填充时间没有效果strictFill 遇到非 null 值用 fillStrategy 或确保字段为 null前端拿到 ID 精度丢失Long 类型序列化问题转 String 或加 ToStringSerializer这张表基本覆盖了 day1 到项目实战初期会遇到的绝大多数问题。如果你是新入行的开发建议把表格对应的情况在本地都复现一遍踩过一遍印象会非常深。6. 结合微服务场景的一点个人体会老实说MyBatis-Plus 学起来不难真正难的是把它放进微服务的整体架构里去思考。课到后面拆服务的时候你会发现几乎所有服务都在依赖这个通用 CRUD 能力但它又不能包打天下。复杂 join 查询、多表事务、高并发更新最终还是得回归手写 SQL 和业务设计。我个人的建议是把 MyBatis-Plus 当成你工具箱里的“快速扳手”而不是唯一的锤子。单表 CRUD、分页、逻辑删除、自动填充这些场景大胆用它遇到复杂查询或者性能瓶颈随时可以用 XML 自定义 SQL 接管。这种“增强但不替代”的定位恰恰是它最聪明的地方。还有一个小技巧想分享给正在上课的同学每学到一个注解或配置就尝试在本地项目里改一个参数看行为变化比单纯做笔记效果好太多。比如把 IdType.AUTO 改成 ASSIGN_ID 跑一遍观察主键值的变化把 TableLogic 摘掉再执行删除看看 SQL 的差别。十分钟的实验比背十遍文档都管用。MyBatis-Plus 的润物细无声之处在于一旦你习惯了它再看那些手写所有 mapper 的项目会发自内心觉得它们应该在工程化上做得更好。这不只是效率问题也是微服务架构下模块自治的必然选择。希望这篇总结能帮你在课程 day1 少走弯路后面学 Spring Cloud 的时候把精力留给真正重要的分布式问题。