Spring Boot 集成 MyBatis Plus 实战:代码生成器与核心插件详解
1. 从一套通用后台说起为什么选择 Spring Boot MyBatis Plus先聊个背景。这几年我参与过的各类企业级项目里至少七成以上的后端服务都是“管理后台 数据中台”这类形态一堆数据表、一堆常规增删改查、再加几个统计报表。这类项目最怕的不是业务复杂而是“重复劳动太多”。每个表都要手动写一套 Mapper、一套 Service、一套 Controller如果团队里没有统一规范十个人能写出十种风格的 CRUD维护成本直线上升。后来我逐渐统一到一套组合拳Spring Boot 负责整体容器与自动配置MyBatis Plus 负责数据访问层的增强。MyBatis Plus 这个框架本质上是 MyBatis 的增强工具只做增强不做改变核心价值在于三点通用 CRUD 不用手写 SQL、条件构造器让动态查询变得非常直观、代码生成器能把建表后的基础代码全部铺好。配合它内置的分页、乐观锁、逻辑删除等插件基本能覆盖日常工作里 80% 以上的数据操作场景。这篇文章我不打算写教科书式的 API 罗列而是按实际项目落地顺序来拆先是环境搭建和代码生成器怎么配置最顺手再把核心插件的原理和适用场景说透最后用一套“工业级 CRUD”的写法把 Service 层、Controller 层、事务和性能优化串起来。中间会穿插我踩过的坑和现在一直在用的习惯。适合谁看刚接触 MyBatis Plus 的初级开发、想优化现有项目代码结构的中级开发以及后端团队里负责技术规范的人。看完之后你可以直接照着这篇的思路去搭自己的项目骨架。2. 项目初始化与依赖选型2.1 依赖版本与引入Spring Boot 和 MyBatis Plus 的版本兼容问题是新人最容易翻车的地方。MyBatis Plus 的版本号体系分两支mybatis-plus-boot-starter和mybatis-plus-spring-boot3-starter。前者对应 Spring Boot 2.x后者对应 Spring Boot 3.x。如果你用的是 Spring Boot 3.x 却引入了旧版 starter启动阶段就会因为包路径变更而报一堆 ClassNotFound 之类的错误。个人目前常用的搭配是这个组合Spring Boot 2.7.x mybatis-plus-boot-starter 3.5.xSpring Boot 3.x mybatis-plus-spring-boot3-starter 3.5.xdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency注意这里不需要再额外引入 MyBatis 和 Spring JDBC 的依赖starter 会自动传递。数据库驱动根据自己的情况加MySQL 8 用com.mysql:mysql-connector-j如果项目里要操作 PostgreSQL 或 Oracle依赖相应替换即可。还有一个细节如果你在同一个项目里同时使用 MyBatis Plus 和 PageHelper这两个分页插件会产生冲突导致方言自动识别错乱。实际项目里选一个就够了既然用了 MyBatis Plus 就不要再叠 PageHelper。2.2 数据源与连接池配置MyBatis Plus 本身不管理数据源它依赖 Spring Boot 的自动配置。所以我们需要在application.yml里配置好数据源和连接池。这里我推荐使用 HikariCPSpring Boot 2.x 之后默认自带零配置就能用性能也够好。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 30000 connection-timeout: 30000 pool-name: HikariCP有几个坑需要注意MySQL 8 的驱动类名必须是com.mysql.cj.jdbc.Driver如果你写的还是老版的com.mysql.jdbc.Driver连接时会提示Public Key Retrieval is not allowed之类的错误。另外serverTimezone一定要设置不设置的话 JDBC 会取本地默认时区在部分云数据库上会出现时间偏移 8 小时的问题。我习惯固定写成Asia/Shanghai避免宿主服务器时区不统一导致的时间不一致。连接池参数这块maximum-pool-size不要盲目贪大。很多新手一上来就配 200觉得这样并发高实际上连接池越大占用数据库连接资源越多反而在高并发下容易出现连接等待超时。一般根据业务并发量20 ~ 50 就足够了。我见过的大多数项目20 左右的池子配上合理的 SQL性能是够用的。2.3 MyBatis Plus 全局配置数据源配好之后还需要在application.yml里配置 MyBatis Plus 的全局行为。这部分配置决定了很多默认策略。mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 banner: false解释一下几个关键配置的含义map-underscore-to-camel-case开启下划线转驼峰映射。数据库字段是create_time实体属性是createTime开启后自动映射不用手写resultMap。id-type: assign_id主键生成策略。MyBatis Plus 默认用的是雪花算法生成一个 Long 类型的主键。这个设计在分布式场景下很实用不用依赖数据库自增。logic-delete-field: deleted全局逻辑删除字段。配置之后所有实体的删除操作会变成update t set deleted 1 where id ?而不是物理delete。这个后面会详细讲。banner: false是关闭启动时的 MyBatis Plus 大字 banner纯属个人审美问题不做也没关系。提示全局逻辑删除配置在 3.5.x 版本里优先级比实体类上的TableLogic低。也就是说如果实体类上单独标注了TableLogic以实体类上的配置为准。建议两种方式选一种不要混用否则排查问题时容易困惑。3. 代码生成器从数据库表到基础代码的自动化3.1 代码生成器的设计逻辑代码生成器是 MyBatis Plus 生态里我非常喜欢的一个功能。它做的事情很简单根据数据库表结构生成对应的实体类、Mapper 接口、Service 接口、Service 实现类、Controller 以及 XML 映射文件。本质上就是把你平时手写那些“没有技术含量却有大量重复”的文件用模板自动拼出来。可能有人会觉得代码生成器生成的代码不够灵活后期反而要改很多。我对这个说法持保留态度代码生成器最大的价值不是“生成的代码永远不用改”而是“把结构化、可预测的部分一次性生成人工只处理业务差异”。比如实体类的字段映射、基础 CRUD 方法这些东西每个表都一样让代码生成器来做既快又标准。真正需要人工介入的是业务校验、关联查询、权限控制这些差异点。3.2 FastAutoGenerator 的使用MyBatis Plus 3.5.x 之后官方推荐使用FastAutoGenerator来生成代码。相比老版的AutoGenerator它的 API 设计更简洁链式调用一目了然。来看一个我实际项目里常用的完整写法FastAutoGenerator.create(jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8, root, root) .globalConfig(builder - { builder.author(dev) .outputDir(/path/to/project/src/main/java) .dateType(DateType.TIME_PACK) .disableOpenDir(); }) .packageConfig(builder - { builder.parent(com.example) .moduleName(system) .entity(entity) .service(service) .serviceImpl(service.impl) .mapper(mapper) .controller(controller); }) .strategyConfig(builder - { builder.addInclude(sys_user, sys_role, sys_user_role) .addTablePrefix(sys_) .entityBuilder() .enableLombok() .logicDeleteColumnName(deleted) .idType(IdType.ASSIGN_ID) .controllerBuilder() .enableRestStyle() .serviceBuilder() .formatServiceImpl(%sServiceImpl); }) .templateConfig(builder - { builder.controller(/templates/controller.java.vm) .service(/templates/service.java.vm) .serviceImpl(/templates/serviceImpl.java.vm) .mapper(/templates/mapper.java.vm) .entity(/templates/entity.java.vm); }) .execute();这段代码干了什么拆开看addInclude指定要生成的表支持多表用逗号分隔。实际项目中强烈建议明确指定表名而不是addAll一次性把所有表都生成出来。有些项目里会有一堆临时表、历史表全量生成会污染代码目录后期还得删。addTablePrefix(sys_)是表前缀过滤。比如数据库表名是sys_user生成实体类时自动去掉前缀变User。这是非常实用的功能很多老库的表名都有统一前缀。enableLombok()让实体类自动生成Data注解不用写成堆的 getter/setter。logicDeleteColumnName(deleted)让生成的实体类带上逻辑删除注解TableLogic。enableRestStyle()让 Controller 使用 RESTful 风格方法上自动加RestController和RequestMapping。3.3 自定义模板解决代码生成器的“不够用”用过一段时间代码生成器后你会发现官方的默认模板在某些细节上不够贴合自己的项目规范。比如默认生成的 Controller 里没有统一返回值ResultT或者 Service 接口里缺少批量插入方法。这时候就需要自定义模板。FastAutoGenerator 允许指定模板路径模板文件可以用 Velocity 语法。我一般会在src/main/resources/templates/下放一套自定义模板。举个例子我改造后的 controller 模板大致长这样package ${package.Controller}; import org.springframework.web.bind.annotation.*; import ${package.Service}.${table.serviceName}; import com.example.common.Result; RestController RequestMapping(/${table.controllerMappingHyphenStyle}) public class ${table.controllerName} { private final ${table.serviceName} ${table.serviceVarName}; public ${table.controllerName}(${table.serviceName} ${table.serviceVarName}) { this.${table.serviceName} ${table.serviceVarName}; } }注意这里我加了构造器注入而不是Autowired字段注入。原因是 Spring 官方更推荐构造器注入它能让依赖关系显式化测试时也更容易 mock。把代码生成器和团队规范结合起来生成的代码才是真正能直接用的代码。3.4 代码生成器的经验之谈用了很久代码生成器最想提醒大家的是代码生成器适合生成实体类、基础 Service 实现、基础 Mapper但不要把复杂查询也交给它。很多新手指望代码生成器连多表联查、子查询都生成出来这不现实。代码生成器的能力边界是“单表结构驱动的代码生成”复杂业务 SQL 还得靠自己写。另外生成代码前建议先检查表结构设计。比如字段注释是否完善、字段类型是否合理、是否有唯一索引、是否预留了deleted、create_time、update_time等通用字段。表结构设计得越规范生成出来的代码质量越高。实际项目中我通常会在建表完成后先跑一次生成器拿到实体、接口和实现骨架然后再花时间在 Service 层添加业务方法。这样整体效率比手写快很多而且代码风格天然统一。4. 核心插件分页、乐观锁、逻辑删除的机制解析4.1 分页插件MybatisPlusInterceptor 的正确配置MyBatis Plus 的分页插件可能是被用得最多、也最容易配错的功能。我先讲一下它背后的拦截机制MyBatis Plus 通过MybatisPlusInterceptor注册多个内置拦截器分页拦截器是其中之一。当执行查询时分页拦截器会拦截 SQL自动生成一条count查询语句用于统计总数再生成一条带LIMIT的查询语句返回数据。配置方式如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }这里有两个参数需要关注setMaxLimit(100L)限制单页最大查询数量。如果不设置攻击者可以拼一个很大的size参数直接把全表数据拉下来这在对外开放的接口里是很严重的安全隐患。setOverflow(false)溢出总页数时是否需要处理。设置为 false查第 99999 页时不会给你继续拼接 SQL而是直接返回空数据。分页使用示例PageUser page new Page(1, 10); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getStatus, 1).orderByDesc(User::getCreateTime); PageUser result userMapper.selectPage(page, wrapper); ListUser records result.getRecords(); long total result.getTotal();注意一个细节Page对象在传入selectPage之后返回的result里才有total数据。你不能直接拿刚才创建的page对象去读getTotal()必须在selectPage调用后重新获取返回值。这是我见过不少人踩过的坑。4.2 乐观锁插件避免并发覆盖的轻量方案先解释“乐观锁”这个概念。在并发场景下多个线程同时读取一条数据并各自更新后提交的事务会覆盖先提交的事务导致数据丢失。解决思路有两类悲观锁数据库行锁和乐观锁版本号判断。MyBatis Plus 的乐观锁插件底层用的是版本号机制每条记录有一个version字段更新时会带上where version ?判断如果版本号匹配则更新并把版本号 1不匹配则影响行数为 0应用程序可以据此判断“数据已被他人修改”做出相应处理。配置如下Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 分页拦截器和乐观锁拦截器可以共存 return interceptor; }实体类里需要加注解Data public class User { TableId(type IdType.ASSIGN_ID) private Long id; private String name; Version private Integer version; }这里有个非常重要的陷阱乐观锁插件只拦截updateById和update(entity, wrapper)这类内置更新方法不拦截手写在 XML 里的自定义 update SQL。如果你自己写了update user set name #{name} where id #{id}那乐观锁完全不起作用。要生效手写 SQL 里必须自己加version判断和自增处理。使用乐观锁还有一个业务上的配套方案更新失败时提示用户“请刷新后重试”或自动重新查询。比如一个编辑表单提交时发现updateById返回 0 行说明版本号不匹配这时要向前端返回明确提示而不是静默成功。我司某个供应链项目里就是因为没做这个失败处理两个人同时修改同一个商品价格后保存的人完全不知道价格被覆盖了后来查了半天数据不一致的问题。4.3 逻辑删除既方便又容易埋坑逻辑删除指的是删除数据时不真正执行 DELETE而是在记录上打标记。MyBatis Plus 里通过TableLogic注解或全局配置实现。我听说过一句话逻辑删除是把双刃剑。使用得当数据可追溯误删可恢复使用不当全表查询里到处都是deleted 0条件性能下降索引失效而且多表 join 时如果忘了加逻辑删除条件会出现“引用已删除数据”的脏数据。MyBatis Plus 的逻辑删除机制是这样的配置了逻辑删除字段后deleteById自动转成update set deleted 1 where id ? and deleted 0所有查询自动追加deleted 0所有更新自动追加deleted 0这里面的隐患在于“所有查询自动追加”。如果一个外键关联表也需要查询已删除的记录比如订单表关联商品表商品被逻辑删除后你想要的是“历史订单仍然能显示商品快照”但自动拼上deleted 0导致查不到。解决办法是用SqlParser(filter true)或者在查询条件里显式处理。实际项目中我建议逻辑删除字段统一命名deleted类型用tinyint取值 0 和 1。如果要支持更丰富的状态可以扩展deleted值比如 2 表示“被系统自动清理”但这时默认的逻辑删除判断就不够用了需要自定义。注意逻辑删除字段建议加入联合索引尤其是未来要做数据归档和清理的场景。否则表数据量上来之后全表扫描一次where deleted 0会非常耗时。4.4 防全表更新与插入插件一次手抖的教训这个插件我一开始觉得没啥用直到有一次在测试环境手抖一个没有 where 条件的更新 SQL 把整张表的所有数据都改了从那以后我再也没有漏配过它。BlockAttackInnerInterceptor是 MyBatis Plus 内置的拦截器用来阻断全表更新和全表删除操作。当检测到没有 where 条件的 update 或 delete 语句时直接抛出异常阻止执行。Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 注意顺序先加防全表更新再加其他拦截器 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; }拦截器顺序有个讲究。分页拦截器、乐观锁拦截器、防全表更新拦截器的执行顺序可能会影响最终效果。我的实践是把防全表更新拦截器放在第一位这样无论是哪个路径触发的 update 操作都能在一开始就被拦下来做安全检查。但也要承认这个插件有个误伤场景业务里确实存在“无条件更新某个状态字段”的需求比如所有用户数据过期。遇到这种情况我建议用两种方式处理在 Service 层先给一个默认条件比如wrapper.ne(id, -1)骗过拦截器。更规范的做法单独写一条 SQL在 XML 里通过where 1 1显式声明“我就是要全表更新”人工确认过再放行。5. 工业级 CRUD 的落地从单表操作到业务闭环5.1 Service 层架构IService 的基座价值MyBatis Plus 提供了IServiceT和ServiceImplM, T这套通用 Service 体系。它最大的好处是内置了一批开箱即用的方法save、saveBatch、updateById、getById、list、page、removeById等等。不需要自己每次去调 mapper直接通过this.xxx()就能完成基础操作。实际项目的 Service 层我一般是这样组织的public interface UserService extends IServiceUser { /** * 创建用户返回包含主键的用户对象 */ User createUser(UserCreateRequest request); /** * 更新用户带乐观锁控制版本 */ boolean updateUser(UserUpdateRequest request); }Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Override public User createUser(UserCreateRequest request) { User user new User(); BeanUtils.copyProperties(request, user); user.setStatus(1); save(user); return user; } }注意BeanUtils.copyProperties这里我用的是 Spring 提供的那个不是 Apache 的。两者拷贝行为略有差异Spring 的在属性类型不匹配时不会抛异常而是跳过适合业务 DTO 转实体这种场景。Service 层还有一个高频操作批量保存。saveBatch内部会拼接 SQL底层按批次提交避免一条条插入导致的性能开销。ListUser users new ArrayList(); for (UserDTO dto : dtoList) { users.add(dto.toEntity()); } boolean result userService.saveBatch(users);默认每批 1000 条如果数据量大可以考虑自定义批次大小但千万别一次插入十万条会超过数据库 max_allowed_packet 限制。我在某个项目中导入 Excel 时遇到过一次这个问题后来把每批改成 500 条就稳定了。5.2 Controller 层拒绝裸奔的 RESTful 规范Controller 层经常是第一个被写烂的层。很多人直接返回ListUser或者User实体没有统一包装没有分页结构封装错误处理更是五花八门。这会导致前端对接时非常痛苦每个接口的返回结构都不一样。我的统一返回结构设计Data public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message OK; r.data data; return r; } public static T ResultT error(int code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }Controller 的示例RestController RequestMapping(/api/v1/users) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } PostMapping public ResultUser create(RequestBody UserCreateRequest request) { return Result.success(userService.createUser(request)); } PutMapping(/{id}) public ResultBoolean update(PathVariable Long id, RequestBody UserUpdateRequest request) { request.setId(id); return Result.success(userService.updateUser(request)); } DeleteMapping(/{id}) public ResultBoolean delete(PathVariable Long id) { return Result.success(userService.removeById(id)); } GetMapping(/{id}) public ResultUser getById(PathVariable Long id) { return Result.success(userService.getById(id)); } GetMapping(/page) public ResultPageUser page(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, UserQueryRequest query) { return Result.success(userService.pageUsers(page, size, query)); } }这套设计有几个明显的优势返回结构统一前端封装请求层时不用做大量兼容使用RequestBody接收客户端请求请求结构可控服务端接口路径用版本前缀/api/v1/后续升级不破坏旧接口5.3 条件构造器 LambdaQueryWrapper 的正确打开方式MyBatis Plus 的条件构造器是日常开发使用频率最高的特性之一。相比直接在注解里写 SQL 字符串LambdaQueryWrapper通过方法引用的方式引用实体类字段编译期就可以检查字段名是否正确不用担心字段名拼错导致运行时才发现问题。看一个典型场景用户列表按用户名模糊搜索、按状态过滤、按创建时间倒序排列。public PageUser pageUsers(int page, int size, UserQueryRequest query) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), User::getName, query.getName()) .eq(query.getStatus() ! null, User::getStatus, query.getStatus()) .orderByDesc(User::getCreateTime); return this.page(new Page(page, size), wrapper); }这里StringUtils.hasText(query.getName())是三参数的固定配合写法。第一个参数是 boolean 条件为true才拼接后面的条件。这样你就不需要写一堆if (xxx ! null)的判断代码可读性高很多。orderByDesc(User::getCreateTime)这种写法也很安全不会出现 SQL 注入问题因为它生成的列名是从属性映射到数据库列名而不是把外部传入的字符串直接拼 SQL。还有一个高级用法多表联查时用自定义 SQL。当涉及多表 join 的场景LambdaQueryWrapper就不够用了。我习惯在 Mapper 接口里定义方法用Select注解直接写 SQL参数用Wrapper类型传进来做条件拼接。Select(SELECT u.*, r.name AS role_name FROM user u LEFT JOIN role r ON u.role_id r.id ${ew.customSqlSegment}) ListUserRoleVO selectUserWithRole(WrapperUser wrapper);这种方式既保留了 MyBatis Plus 条件构造器的动态拼接能力又拿到了自定义 SQL 的完整控制权。但要注意customSqlSegment里的条件列名必须是数据库真实列名不能再用实体属性名否则会映射失败。5.4 事务管理注解之外还有传播行为CRUD 里的事务最直观的需求是多个写操作要么全成功要么全失败。Spring 的Transactional注解是最常用的手段。Transactional(rollbackFor Exception.class) public boolean transferBalance(Long fromUserId, Long toUserId, BigDecimal amount) { User from userService.getById(fromUserId); User to userService.getById(toUserId); from.setBalance(from.getBalance().subtract(amount)); userService.updateById(from); to.setBalance(to.getBalance().add(amount)); userService.updateById(to); return true; }注意这里我特意写了rollbackFor Exception.class。这是很多人容易忽略的一个细节Transactional默认只在遇到RuntimeException和Error时回滚如果业务方法里抛的是受检异常比如自定义的BizException extends Exception默认不会回滚数据就会出现“半成品”状态。所以做业务事务时我习惯直接指定rollbackFor把受检异常也纳入回滚范围。事务传播行为多方法嵌套时也要关注。举个实际场景批量导入用户每个用户可能触发单独的业务校验逻辑。如果内层方法期望“某个失败不影响其他用户正常导入”内层方法就应该是REQUIRES_NEW或者NOT_SUPPORTED如果希望某个失败整个批次回滚就保持默认的REQUIRED。这个决策完全取决于业务诉求千万不要一刀切。5.5 性能优化从慢 SQL 到 N1 查询CRUD 写完了性能问题就开始浮现。我最常处理的两类问题慢 SQL 和 N1 查询。慢 SQL 的排查我一般先开 MyBatis Plus 的 SQL 日志再结合数据库的EXPLAIN查看执行计划。日志配置上面已经展示过log-impl: StdOutImpl开发环境可以开着生产环境建议换成 logback 或者 log4j2 的统一输出不要直接打到控制台。N1 查询这个典型问题通常出现在列表页展示关联数据时。比如用户列表要显示每个用户的部门名称如果你在循环里调用departmentService.getById()数据量 100 条时就是 1 条主查询 100 条关联查询就是 101 条 SQL 打在数据库上。解决办法是批量查询ListUser users userService.list(); ListLong deptIds users.stream().map(User::getDeptId).distinct().collect(Collectors.toList()); ListDepartment departments departmentService.listByIds(deptIds); MapLong, Department deptMap departments.stream().collect(Collectors.toMap(Department::getId, d - d));然后内存里做映射这样 SQL 数量从 101 条降到 2 条。这种优化思路在绝大多数场景下都比复杂的 SQL 联查更简单、更易维护。再提醒一个隐蔽的坑MyBatis Plus 生成的分页 count 语句在join场景下有时候会把主表信息和明细表混在一起统计导致 total 偏大或偏小。遇到这种情况一般建议对 count 语句做单独处理或者直接用自定义 SQL 写分页查询绕开内置 count。6. 实操中常见的坑与解决办法6.1 字段自动填充create_time 与 update_time 的处理很多表结构里有create_time和update_time两个字段。MyBatis Plus 并没有自动填充时间的机制它只做字段映射。如果你不主动维护这两个字段插入和更新时它们就是null。解决方案是自定义自动填充处理器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()); } }实体类上对应的字段加上自动填充注解TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;这样当你调用save或updateById时时间字段会被自动填充。注意strictInsertFill会检查实体对应字段是否已经有值如果有值就不覆盖避免把业务主动设置的值冲掉。6.2 Jackson 序列化与 Long 精度丢失这个坑如果项目用的是前后端分离几乎必踩。MyBatis Plus 默认的主键策略ASSIGN_ID生成的是 19 位 Long 类型数字。JavaScript 的Number类型只能安全表示 2 的 53 次方以内的整数超过这个范围精度会丢失。也就是说前端拿到用户 id 后回传时最后几位可能已经变了导致操作错误的记录。解决办法在序列化层面把 Long 转成 String。Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); }; }转成字符串后前端拿到的 id 是字符串回传时精度不会丢失。也有人觉得用String类型作为主键更省事但雪花算法配合Long 序列化转换是常见的成熟方案类型语义也更明确。6.3 批量操作与数据库连接池耗尽批量导入、批量更新这类场景容易把连接池打满。原因是每次操作都占用一个连接SQL 执行慢的时候连接被占着不放新的请求拿不到连接池里的空闲连接就排队等待。一旦排队过多就报connection is not available之类的错误。解决方向有三个检查单个 SQL 的执行时间慢 SQL 先优化不要让一条超长 SQL 占着连接不动。调整连接池参数适当调大maximum-pool-size配合connection-timeout设置合理的等待时间。拆分批量任务用异步线程分批执行降低单一批次对连接池的瞬时压力。6.4 常见问题速查表场景现象排查方向与建议分页查询 total 始终为 0页面正常分页但总数不显示确认是否使用selectPage的返回值去读 total而不是创建好的 Page 对象乐观锁不生效并发更新后版本号未改变检查实体类是否有Version注解检查是否走了自定义 update SQL逻辑删除后查询不到数据多表关联查询时关联表数据丢失确认自动追加的deleted 0条件是否符合业务语义必要时特殊处理批量插入报 max_allowed_packet数据量太大超过 MySQL 单次包大小控制每批插入条数500 条内或调整数据库 max_allowed_packetSpring Boot 3 启动失败找不到 mybatis 相关的 class确认引入的是mybatis-plus-spring-boot3-starter不是旧版 starter时间字段差 8 小时数据库时间与本地时间不一致确认 JDBC 连接串设置了serverTimezoneAsia/Shanghai7. 这套组合还能扩展什么写到这里基础实战已经覆盖得比较完整了。最后聊几个实际项目里可能会遇到的扩展方向。7.1 与多数据源结合项目切分读写分离或者接多个业务库时MyBatis Plus 官方提供了DS注解和动态数据源方案。通过DynamicDataSourceAutoConfiguration可以配置主库、从库在 Service 或 Mapper 方法上用注解切换数据源。使用时需要引入dynamic-datasource-spring-boot-starter并配置多个数据源连接。需要注意的一点是事务和数据源切换是有顺序性的。如果你在 Service 方法上既有Transactional又有DS数据源切换要在事务开启之前完成否则可能在事务代理层面就报了数据源冲突。我遇到过的问题是在一个事务方法里中途切换数据源结果后续 SQL 还是走了事务开启时的那个数据源排查了半天才发现是动态数据源切换和事务的集成顺序问题。7.2 与代码评审规范结合代码生成器最被低估的价值其实是“统一代码规范”。团队里最痛苦的事情是代码风格分裂一个实体类三种写法一个 Controller 五种路由风格。用代码生成器把骨架定死新人入职后照着生成器产出写业务代码风格天然统一。配合我在前文介绍的自定义模板团队完全可以把自身规范沉淀到模板里这就是工业级和玩具级的区别。7.3 与缓存层结合CRUD 写多之后读性能紧张是很常见的。MyBatis Plus 本身不管缓存但它生成的代码非常适合按实体维度做 Redis 缓存。我目前的处理模式是数据更新频率低、查询频率高的业务在 Service 层手动做缓存逻辑。缓存需要注意两个问题缓存穿透和缓存一致性。穿透问题可以通过设置空缓存、布隆过滤器解决一致性问题建议在更新数据时先更新数据库再删除缓存而不是先删缓存再更新数据库因为后者在并发场景下会出现短暂的不一致窗口。8. 最后分享几点真实的项目体会这几个项目做下来我最大的体会是MyBatis Plus 这类工具型框架真正体现水平的不是你会调用多少 API而是怎么理解它背后运行的机制知道每一个自动化操作在什么条件下生效、什么条件下失效。分页插件的拦截逻辑、乐观锁不作用于自定义 SQL、逻辑删除自动追加条件的边界——这些知识在文档里都有但在踩坑之前往往不会真正变成你自己的经验。代码生成器可以帮你省下大量重复工作量但它不适合处理复杂查询。业务开发的本质还是理解数据和业务之间的映射关系。把实体、Service、Controller 的分层职责搞清晰把条件构造器、分页、乐观锁等核心插件用对场景平时注意那几句“毫不起眼”的配置背后的含义这套组合拳足够稳定地支撑起一个中小规模业务系统的大半需求。如果你正在搭一个新的后端项目我建议先把本文提到的 MybatisPlusConfig、逻辑删除、统一返回结构、自动填充处理器这几个基础件搭好再配合代码生成器建模块跑通一条链路。这套地基打完之后后面每增加一张表的成本都会降到非常低。希望这篇文章能帮你少踩几个坑。遇到具体问题时欢迎交流讨论。

相关新闻

nanoMuse 浙大开源版:轻量化图像生成模型本地部署与调参实战

nanoMuse 浙大开源版:轻量化图像生成模型本地部署与调参实战

1. 从标题拆解 nanoMuse 的核心定位1.1 这个项目到底在解决什么问题第一次看到“Muse 浙大开源版 nanoMuse”这个标题,我的直觉是:这大概率是一个把原本体量偏大、部署门槛偏高的生成式模型,压缩到能在消费级硬件上跑起来的轻量化版本。事实也…

2026/10/14 4:07:47 阅读更多 →
Midjourney手册实战拆解:从参数到工作流的完整指南

Midjourney手册实战拆解:从参数到工作流的完整指南

简介:这份Midjourney手册是一份完整的中文入门教程,面向零基础设计师、绘画爱好者与内容创作者,帮助读者从注册账号到掌握AI绘图核心操作。全教程约1.3万字、共135页,内容系统覆盖AI绘图前景分析、Midjourney与Discord的基本概念、…

2026/10/14 4:07:47 阅读更多 →
OpenClaw 本地智能体实操,零基础在 Windows 完成部署

OpenClaw 本地智能体实操,零基础在 Windows 完成部署

Windows 一键部署 OpenClaw 教程|5 分钟搞定本地 AI 智能体,告别复杂配置 说明:本文适配 CSDN 发布,全部版本号、链接、文件大小已替换,删除标签 / 日期 / 阅读量 / 示意图,规避禁用词汇,首次全…

2026/10/14 4:07:47 阅读更多 →

最新新闻

Qt Linux打包失败原因与linuxdeployqt避坑指南

Qt Linux打包失败原因与linuxdeployqt避坑指南

简介:本资源是一份面向Linux Qt开发者的技术实践指南,聚焦Ubuntu环境下使用linuxdeployqt工具完成Qt程序跨机部署的核心痛点。针对无Qt运行环境的目标机器,系统性梳理了从Qt开发环境配置、linuxdeployqt源码编译(含Ubuntu 18.04兼…

2026/10/14 4:46:57 阅读更多 →
开放权重模型:可控部署、商用微调与硬件感知推理实战

开放权重模型:可控部署、商用微调与硬件感知推理实战

1. 项目概述:一场被公开谈论的“开放权重”战略卡位战最近刷到一条消息,说英伟达正在和一家叫 Reflection AI 的初创公司洽谈收购或追加投资——这事本身不稀奇,大厂盯上潜力股是常态。但真正让我多看了两眼的,是它背后那个反复出…

2026/10/14 4:46:57 阅读更多 →
Linux系统工具pstack二进制瘦身实战:ELF优化与C语言编译精简

Linux系统工具pstack二进制瘦身实战:ELF优化与C语言编译精简

1. 一个被低估的系统工具&#xff1a;pstack 的“瘦身”为什么值得较真很多人第一次听说 pstack&#xff0c;是在某个进程卡死、CPU 疯狂打满、日志毫无输出的深夜。运维同事甩来一句&#xff1a;“快&#xff0c;pstack 看下堆栈&#xff01;”——于是你敲下pstack <pid&g…

2026/10/14 4:46:57 阅读更多 →
PS5数据管理方案:AnyPS5搞定存档备份与远程串流

PS5数据管理方案:AnyPS5搞定存档备份与远程串流

1. 为什么需要AnyPS5方案PS5入手之后&#xff0c;很多玩家都会遇到同一个问题&#xff1a;主机硬盘看着不小&#xff0c;真用起来却总是捉襟见肘。一款大型游戏动辄上百GB&#xff0c;加上截图、录像、存档备份&#xff0c;内置SSD没过多久就亮红灯。更麻烦的是&#xff0c;一旦…

2026/10/14 4:46:57 阅读更多 →
实时事件分析系统架构设计与Flink+Kafka+ClickHouse实战

实时事件分析系统架构设计与Flink+Kafka+ClickHouse实战

1. 从“rea”这个标题说起&#xff1a;一个被低估的缩写背后藏着什么第一次看到“rea”这个标题的时候&#xff0c;我脑子里蹦出来的第一反应是——这大概率又是一个被缩写玩坏的项目名。做技术的人都有个通病&#xff0c;喜欢把长名字砍成三四个字母&#xff0c;仿佛名字越短越…

2026/10/14 4:46:57 阅读更多 →
一文搞懂MySQL socket连接:原理、配置与2002/2003故障排查

一文搞懂MySQL socket连接:原理、配置与2002/2003故障排查

一次线上事故让我把socket连接这件事彻底搞明白了。当时某个项目组反馈所有本地应用突然连不上MySQL&#xff0c;我登录服务器一看&#xff0c;mysqld进程明明活着&#xff0c;3306端口也在正常监听&#xff0c;可本地应用全部报socket错误。查了半个多小时&#xff0c;最后发现…

2026/10/14 4:45:56 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起&#xff1a;它到底想解决什么问题第一次看到"AnyPS5"这个标题&#xff0c;我脑子里冒出来的第一个念头是&#xff1a;这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述&#xff1a;Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师&#xff0c;我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性&#xff0c;实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景&#xff0c;也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间&#xff0c;确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中&#xff0c;这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →