news 2026/10/2 2:05:29

Spring Boot 3 + MyBatis-Plus 3.5.9 实战:CRUD、分页与性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 3 + MyBatis-Plus 3.5.9 实战:CRUD、分页与性能优化

1. 项目背景与整体设计思路

1.1 为什么在这个时间节点选择Spring Boot 3

我最近在一个新项目里把技术栈切到了 Spring Boot 3 + MyBatis-Plus 3.5.9,整体体验下来确实有不少值得说的东西。先说结论:如果你是一个新启动的 Java 后端项目,现在可以直接上 Spring Boot 3 了,不用担心生态不成熟的问题。Spring Boot 3 从 2022 年底发布至今,经过几个大版本的迭代,稳定性和生态兼容性都已经过了最佳磨合期,MyBatis-Plus 3.5.9 也是在这个背景下对 Spring Boot 3 做了完整适配的关键版本。

Spring Boot 3 最大的变化是底层基于 Spring Framework 6,JDK 基线提升到了 17。这意味着很多老项目里用的javax前缀包名全部改成了jakarta,如果你过去写过import javax.servlet.*这样的代码,迁移到 Spring Boot 3 时第一步就是把这些导入改成jakarta.servlet.*。这个改动本身很机械,但涉及面广,特别是你自己封装了拦截器、过滤器、监听器这些组件的时候,每一处都要检查。另外一个值得关注的点是 Spring Boot 3 默认支持原生镜像编译(AOT),虽然我们这次没有直接用 GraalVM,但如果你未来有云原生场景的部署需求,提前站在 Spring Boot 3 这一侧,后面做适配的成本会低很多。

选型本质上是做一个取舍:是用成熟的 Spring Boot 2.x + MyBatis-Plus 老版本继续“稳”,还是用 Spring Boot 3 这条新主线。

我的建议是,老项目没必要强行迁移,风险大于收益;但新项目直接选 Spring Boot 3,后续两年内不会有技术债务。

1.2 MyBatis-Plus 3.5.9 在整套架构中的角色

MyBatis-Plus 在 Java 后端的地位类似于“增强版 MyBatis 工具箱”。它做的事情本质上是把 MyBatis 中大量重复的 CRUD 模板操作做了抽象和封装,让你不用每个实体类都写一份 XML 映射文件和对应的 Mapper 接口方法。3.5.9 这个版本有几个值得注意的更新点:修复了旧版本中分页插件在某些极端 SQL 场景下的拦截异常,增强了saveBatch批量操作在非 MySQL 数据库上的兼容性,同时对 Spring Boot 3 的自动配置做了更细致的条件化处理。

从架构设计角度看,MyBatis-Plus 扮演的是“数据访问层半成品”的角色——它把你的 Mapper 接口变成了一组现成的方法集合,包括selectById、selectList、insert、updateById、deleteById这些基础操作,不需要你写任何 SQL。但需要清醒认识到:MyBatis-Plus 解决的是 80% 的简单 SQL 场景,剩下的 20% 复杂查询仍然需要你手写 SQL。在项目规划阶段就要把这个边界划清楚,否则团队里容易出现“为了不用写 SQL 而硬用 LambdaQueryWrapper 拼复杂条件”的畸形代码。

我这次项目的整体设计思路是:用 MyBatis-Plus 提供通用 CRUD 骨架,用自定义 Service 层做业务扩展,用 XML 文件承载复杂查询。这套组合既能保证日常开发的效率,又不会在遇到复杂业务时被框架能力卡住脖子。

2. 环境准备与依赖配置实战

2.1 JDK 版本与开发环境选型

Spring Boot 3 的 JDK 基线是 17,这是硬性门槛。如果你本机还在用 Java 8,那第一步就是装 JDK 17。这里有一个实操层面比较重要的点:JDK 17 和 JDK 8 在长期支持(LTS)策略上都属于长期支持版本,但 Spring Boot 3 官方推荐的搭配是 JDK 17 或更新版本。项目里我建议直接用 JDK 17,因为 JDK 21 虽然也已经进入 LTS 序列,但部分云厂商的基础镜像还停留在 JDK 17 层面,部署环境的兼容性要提前确认。

另外开发工具方面,IDEA 需要升级到较新的版本(2023.1 以后对 Spring Boot 3 项目的识别更友好),VSCode 用户注意安装 Spring Boot Extension Pack 和 Java Extension Pack 的最新版本。这里有个小坑:旧版本 IDEA 在导入 Spring Boot 3 项目时,Maven 自动导入阶段有时会报Unresolved dependency: org.springframework.boot:spring-boot-starter-parent这类错误,多半不是依赖本身的问题,而是 IDEA 的 Maven 插件缓存太旧导致的,清一下缓存重启就好。

2.2 Maven 依赖配置与踩坑记录

直接上依赖配置,这是我在项目里实测可用的版本组合:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.4</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <mybatis-plus.version>3.5.9</mybatis-plus.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里有一个特别容易踩的坑:MyBatis-Plus 在 Spring Boot 3 集成时,不能再使用老版本的mybatis-plus-boot-starter,必须使用mybatis-plus-spring-boot3-starter。这个 starter 从 MyBatis-Plus 3.5.4 版本开始提供,专门针对 Spring Boot 3 的自动配置机制做了适配。如果搞混了,启动时大概率会报ClassNotFoundException: jakarta.servlet.ServletException之类的问题,因为老版本 starter 依赖的是 javax 包体系。

另一个细节是 MySQL 驱动。Spring Boot 3.x 的依赖管理默认拉取的是com.mysql:mysql-connector-j,这也是官方推荐使用的新坐标。如果你原来项目里用的是mysql:mysql-connector-java,在 Spring Boot 3 中需要更换坐标。这个坐标变更并不复杂,但不要忽略。

2.3 配置文件编写:数据源与 MyBatis-Plus 关键配置

依赖配好后,下一步是写application.yml。我习惯性地把 MyBatis-Plus 相关的配置单独整理出来,方便后面对照调整:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity

map-underscore-to-camel-case这个配置建议在 3.5.x 版本中显式开启。虽然 MyBatis-Plus 默认已经开了驼峰映射,但显式写出来能让团队里所有人都明确这个约定。id-type: assign_id用的是雪花算法生成 ID,这是分布式场景下的合理默认值,比数据库自增主键更适合在分库分表场景中保持全局唯一。

逻辑删除这块建议优先配置好,logic-delete-field指定实体类中的逻辑删除字段名,logic-delete-value和logic-not-delete-value定义删除和未删除的值。注意逻辑删除和唯一约束有天然冲突,比如用户表里手机号加了唯一索引,逻辑删除后手机号字段还在数据里,下次新用户注册同一手机号时就会唯一索引冲突。这个问题不是 MyBatis-Plus 特有,而是逻辑删除方案的固有问题,后续文章里我会单独展开。

3. 通用 CRUD 服务封装与实战解析

3.1 BaseMapper:零 SQL 完成单表 CRUD

MyBatis-Plus 最核心的价值在于BaseMapper<T>接口。你的 Mapper 接口只要继承它,就自动获得了十几个单表 CRUD 方法。看一个实际例子:

public interface UserMapper extends BaseMapper<User> { // 这里不需要写任何方法,selectById/selectList/insert/updateById/deleteById 都自带 // 复杂查询仍然自己写 List<User> selectByCondition(@Param("name") String name, @Param("age") Integer age); }

对应实体类:

@Data @TableName("t_user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; @TableLogic private Integer deleted; }

这里有个注解细节:@TableName("t_user")用来指定实体类对应的表名。如果代码里表名字段和数据库表名不一致,这个注解必须写,否则 MyBatis-Plus 会默认把User映射到user表,运行时直接报“表不存在”。@TableId(type = IdType.ASSIGN_ID)指定主键生成策略,用雪花算法自动填充 ID。如果不加这个注解且主键字段名不是id,MyBatis-Plus 在插入时会因为不知道哪个字段是主键而报错。

之前项目里遇到过一个小问题:数据库表设计里主键字段叫user_id,实体类字段叫userId,这种情况就必须用@TableId(value = "user_id")显式指定。不要指望 MyBatis-Plus 能从userId智能推测出它对应user_id还是id,注解写清楚最保险。

3.2 Service 层封装:避免每个 Service 重复造轮子

Service 层的封装是提升开发效率的关键一步。MyBatis-Plus 提供了IService<T>接口和ServiceImpl<M extends BaseMapper<T>, T>实现类,基于它们可以快速构建一套标准的 Service 层。但实际开发中,我更推荐在这个基础上再做一层简单的抽象,统一处理返回结果和异常:

public interface IBaseService<T> extends IService<T> { <V> V getByIdDetail(Long id, Class<V> voClass); boolean saveBatchQuietly(Collection<T> entityList); }

不过这里要克制一个冲动:不要试图把 Service 层设计得过于“通用”。Service 层的核心职责是承载业务逻辑,如果一个方法所有实体类都能用同一个模板完成,那它大概率属于 BaseMapper 的能力边界,不需要在 Service 层重复包装。我见过不少项目在 Service 层封装了一堆saveOrUpdateBatch、listByWrapper的透传方法,最后维护成本比直接用 BaseMapper 还高。通用 CRUD 的正确姿势是让框架帮你处理那些完全无业务逻辑的单表操作,业务方法则老老实实在各自 Service 里写。

3.3 通用 CRUD 接口的一层表现层设计

很多项目会在 Controller 层直接注入 Mapper 来执行查询,这种直连数据层的方式维护起来会非常痛苦。一个更合理的方式是在系统边界层做统一封装,包括统一响应结构、统一异常拦截、统一的参数校验入口。

我建议 Controller 层这么设计:

@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/{id}") public Result<UserVO> getUserById(@PathVariable Long id) { User user = userService.getById(id); if (user == null) { return Result.fail(404, "用户不存在"); } UserVO vo = new UserVO(); BeanUtils.copyProperties(user, vo); return Result.ok(vo); } @PostMapping public Result<Long> createUser(@Validated @RequestBody UserCreateDTO dto) { User user = new User(); BeanUtils.copyProperties(dto, user); userService.save(user); return Result.ok(user.getId()); } @DeleteMapping("/{id}") public Result<Void> deleteUser(@PathVariable Long id) { userService.removeById(id); return Result.ok(null); } }

这个设计有几个要点:Controller 不直接操作 Mapper,而是调用 Service;入参用 DTO 而非实体类,避免前端字段直通数据表造成信息过曝;出参用 VO 而非实体类,避免把deleted、create_time这类字段直接返给前端。BeanUtils.copyProperties这种拷贝方式会造成一定的性能开销,但可读性高,适合中小规模项目,需要极致性能的场景可以后续改成 MapStruct。

4. 核心功能实现:分页、批量与 LambdaQueryWrapper

4.1 分页插件配置与分页查询实操

分页是实际项目中使用频率最高的功能,MyBatis-Plus 的分页插件也一直是它的王牌能力。分页插件不是默认开启的,需要手动配置拦截器:

@Configuration @MapperScan("com.example.demo.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

如果没有注册这个拦截器,调用page方法时,数据能查出来但不会执行分页统计,返回的total会是 0,这个坑非常隐蔽。我见过不止一个同事在启动日志里没看到SELECT COUNT(*)语句,还以为是 MyBatis-Plus 的 bug,实际上就是拦截器没注册。

分页查询的标准写法:

Page<User> page = new Page<>(current, size); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), User::getName, name) .ge(age != null, User::getAge, age) .orderByDesc(User::getCreateTime); Page<User> result = userMapper.selectPage(page, wrapper);

分页插件生成的 COUNT 语句在某些复杂 SQL 下会报错,比如查询里包含distinct或者多表 join 时。3.5.9 版本在 count 优化上做得很好,但如果遇到自定义 SQL 的分页统计不准时,可以考虑用@InterceptorIgnore注解跳过拦截器处理,或者自己写一个独立 count 查询。实际上 3.5.9 的PaginationInnerInterceptor对 join 查询的 count 处理已经足够聪明,它会尝试优化成轻量的子查询统计。

还有一个值得注意的细节:分页插件默认的maxLimit是不受限的。如果调用方传了一个非常大的current和size,一次查询可能要扫描大量数据。建议在插件上设置一下最大限制:

PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(500L);

这样超过 500 条的单页查询会自动被限制住,避免恶意或误操作导致数据库压力陡增。

4.2 批量操作:saveBatch 与自定义批量 SQL

批量插入是高频场景之一。MyBatis-Plus 的IService.saveBatch方法可以批量保存实体集合,底层默认执行的是逐条 INSERT,虽然框架内部用 ExecutorType.BATCH 模式做了一轮优化,但在数据量较大时性能还是不够理想。测试下来,在 MySQL 上插入一万条数据,逐条插入需要 10 秒以上,改成拼接多值 INSERT 语句后可以压到 1 秒以内。

如果对批量插入性能有要求,推荐自定义 SQL:

<insert id="insertBatch"> INSERT INTO t_user (id, name, age, email, deleted) VALUES <foreach collection="list" item="item" separator=","> (#{item.id}, #{item.name}, #{item.age}, #{item.email}, 0) </foreach> </insert>

对应 Mapper 接口:

int insertBatch(@Param("list") List<User> users);

使用这个方式要注意 SQL 长度限制。MySQL 的 max_allowed_packet 默认是 64MB,每一批插入 500~1000 条是比较稳妥的量级。我项目里封装一个批量插入方法,每次分批 500 条循环插入,既保证了单次 SQL 的体量不会超限,又保证了整体插入效率:

@Transactional(rollbackFor = Exception.class) public void batchInsertUsers(List<User> userList) { if (CollectionUtils.isEmpty(userList)) { return; } List<List<User>> partition = Lists.partition(userList, 500); for (List<User> batch : partition) { userMapper.insertBatch(batch); } }

注意事务控制:批量操作一定要加上@Transactional,并且要明确rollbackFor = Exception.class。默认的@Transactional只对RuntimeException回滚,如果业务方法抛了受检异常则不会触发回滚,这是个高频坑点。

批量更新方面,saveBatch对应的updateBatchById也是逐条 UPDATE,数据量大时性能同样堪忧。真正高效的批量更新方案通常需要基于 CASE WHEN 语法,在 XML 里用 foreach 动态拼接多个更新条件,这是 MySQL 8.0 中比较推荐的做法。

4.3 LambdaQueryWrapper 的取舍与复杂查询边界

LambdaQueryWrapper 是 MyBatis-Plus 中最常用的构造器,它用 Lambda 表达式引用实体类字段,避免在代码里以字符串硬编码数据库字段名。随手写一个组合查询:

List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>() .eq(User::getStatus, 1) .likeRight(User::getName, "张") .between(User::getAge, 18, 35) .in(User::getRoleId, Arrays.asList(1L, 2L, 3L)) .orderByDesc(User::getCreateTime) .last("limit 10"));

likeRight会生成LIKE '张%',走索引比前置通配符%张更友好。.last("limit 10")这种写法虽然方便,但要注意:如果拼接了用户可控的字符串,会有 SQL 注入风险,绝对不要让任何变量进入.last()方法。

复杂查询的边界在 AbstractWrapper 上需要特别注意。举个例子:OR 条件组合、子查询这些场景,用 Wrapper 拼起来的可读性很差。当 Wrapper 出现三层以上嵌套、或者需要关联多张表时,就该回到 XML 文件手写 SQL。这是我项目里一条不成文的硬性约定,团队代码 review 时也会重点检查这块。

随意设置一个经验值:单表查询且条件在 5 个以内,用 LambdaQueryWrapper;超过这个阈值或者涉及 join,就建立对应 VO 类 + XML 手写 SQL。

5. 日志配置:logback 与 log4j2 实战对比

5.1 Spring Boot 3 中如何切换日志框架

日志配置在真实项目里远比大多数人想象的更重要。Spring Boot 3 默认使用 Logback 作为日志实现,同时支持 Log4j2 切换。两者的核心差异:Logback 更稳定、接入零成本,异步日志配置简洁;Log4j2 在高并发场景下吞吐量优于 Logback,且支持无垃圾日志模式,但配置复杂度更高。

网上搜“springboot3 log4j2”能搜到一堆配置教程,这里直接给结论:中小型项目用默认的 Logback 完全足够,压测阶段如果发现日志同步写入会降低吞吐量,就开启 Logback 异步写入;追求极致性能且团队有能力维护复杂配置时再考虑 Log4j2。日志框架切换的代价不仅是依赖调整,还包括配置格式、归档策略、与 skylog/SLS 等日志收集服务的对接方式全面变化,这些隐形成本经常被忽略。

如果确实要切换到 Log4j2,需要手动排除默认日志依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>

切换之后需要新建log4j2-spring.xml配置文件。Spring Boot 环境下 Log4j2 的配置文件命名有讲究:必须使用log4j2-spring.xml而不是log4j2.xml,这样才能触发 Spring Boot 对 Log4j2 的自动增强处理。

5.2 logback-spring.xml:一套可复制到生产环境的配置

如果你选择留在 Logback,我推荐直接把这个logback-spring.xml文件复制到src/main/resources下,把应用名改掉就能直接用:

<?xml version="1.0" encoding="UTF-8"?> <configuration> <springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/> <property name="LOG_HOME" value="${LOG_PATH:-./logs}/${APP_NAME}"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%thread] %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/info.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%thread] %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>60</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%thread] %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE_INFO"/> <appender-ref ref="FILE_ERROR"/> </root> <logger name="com.example.demo.mapper" level="DEBUG"/> </configuration>

这里有几个关键细节。springProperty可以从application.yml中读取配置,把应用名拼进日志目录,这样多应用共享一个日志目录时不容易混淆。FILE_INFO和FILE_ERROR分开写,排查问题时直接看 error.log,不用在大文件里翻找异常信息。把com.example.demo.mapper包的日志级别设为 DEBUG,MyBatis 会打印完整 SQL 语句,开发环境调试非常有用。但在生产环境中建议去掉或者至少改成 INFO,否则 SQL 日志量大到能拖慢应用。

如果你要打印 MyBatis-Plus 的 SQL 日志,还需要在application.yml中设置:

mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

设置之后,Mapper 包名下的 DEBUG 日志就会被输出到控制台。使用StdOutImpl会直接打印到标准输出,不走日志框架,生产环境不要配置这个。

6. 常见问题与排查心得

6.1 高频报错速查表

整理一份近半年在实际项目中遇到的最常见问题,直接对应解决办法:

报错/现象根本原因解决方案
Invalid bound statement (not found)Mapper 接口与 XML 文件映射失败检查 XML 文件的 namespace 是否与接口全限定名一致;确认mapper-locations配置路径正确;确保 XML 中方法 id 与接口方法名一致
Table 'xxx' doesn't exist实体类映射的表名不正确检查数据库表名是否与实体类驼峰转换后结果一致,不一致时使用@TableName显式指定
ClassNotFoundException: javax.servlet.*使用了旧版 MyBatis-Plus starter改用mybatis-plus-spring-boot3-starter
分页查询 total=0分页拦截器未注册添加MybatisPlusInterceptor,注册PaginationInnerInterceptor
使用saveBatch插入非常慢逐条 INSERT 性能瓶颈改用自定义多值 INSERT 批量 SQL,每批 500 条
逻辑删除后唯一索引冲突逻辑删除方案的固有问题方案一:把逻辑删除字段加入唯一索引;方案二:用删除时间戳+手机号做唯一约束;方案三:改为物理删除
启动时打印大量无用的查询日志Mapper 包 DEBUG 日志未关生产环境设置 Mapper 包级别为 INFO,去掉 log-impl 或改为 Slf4jImpl 并关闭 DEBUG 级别输出
时间字段查询范围不正确时区配置不对JDBC URL 加serverTimezone=Asia/Shanghai,统一全局时区

6.2 几个容易让新手头晕的非直观问题

第一个问题是实体类字段中 boolean 类型与数据库字段的映射关系。Java 的boolean类型在 MyBatis-Plus 中会自动映射到数据库的tinyint(1),但如果在 Lombok 的 @Data 下生成的是isDeleted()方法名,某些配置下可能跟 MyBatis 的属性解析不一致。建议尽量用Integer表示逻辑标志位,避免这类隐性坑。

第二个问题是删除方法与@TableLogic的联动。配置了逻辑删除后,deleteById方法不会真正执行 DELETE 语句,而是生成一条 UPDATE 语句把逻辑删除字段置为 1。这意味着后续所有查询都会自动追加deleted=0条件。但如果 SQL 是通过 XML 自定义写的,就需要手动在 where 条件里加deleted=0,否则会把逻辑删除的数据也查出来。3.5.9 版本对自定义 SQL 中的逻辑删除判断没有做自动增强,这是很多人排查了半天才发现的问题。

第三个问题是特殊字符转义。MyBatis-Plus 的like方法传入包含%、_的字符串时不会自动转义,会导致 LIKE 查询匹配到意外数据。需要手动处理:

String escapedName = name.replace("\\", "\\\\") .replace("%", "\\%") .replace("_", "\\_");

这在搜索场景里算是高频问题了,用户输入一个%就可能把全表数据查出来,不仅性能有问题,还有数据泄露风险。

6.3 性能优化的几个实操建议

除了前面提到的批量插入优化,还有几个性能细节值得注意。

第一,避免在循环里单条查询数据库。常见的 N+1 问题在 ORM 框架中大量存在,MyBatis-Plus 提供了listByIds方法,一次查询获取主键集合对应的全部数据。循环里拼selectById的做法在数据量增大后性能会肉眼可见地下滑。

第二,查询时只查需要的字段。MyBatis-Plus 的 LambdaQueryWrapper 支持.select()指定查询列:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.select(User::getId, User::getName, User::getAge);

当表字段很多、且某条业务链路上不需要全部字段时,这个操作能明显减少网络传输和实体转换开销。尤其在设计 VO 查询时,这种精确选列的方式让 SQL 意图非常清晰。

第三,数据库表必须建立合适的索引。MyBatis-Plus 的分页 ORDER BY 字段如果不走索引,数据量过百万后深分页会非常慢。建议分页排序字段加复合索引,必要时可以使用 MySQL 8.0 的降序索引特性,并尽量避免深偏移,比如用WHERE id > #{lastId} LIMIT 20方式代替LIMIT 100020, 20。

6.4 MyBatis-Plus 3.5.9 与 Spring Boot 3 的版本兼容性

兼容性这个问题值得单独说。Spring Boot 3.x 版本更新很快,从 3.0 到 3.2 再到 3.3,每个小版本都会调整部分自动配置逻辑。MyBatis-Plus 3.5.9 官方声明支持的 Spring Boot 版本是 3.0 到 3.3。使用 Spring Boot 3.2.x 搭配 3.5.9 是我目前最推荐的生产组合,稳定性经过大规模验证。

如果你在 Spring Boot 3.4 或更高版本上使用 MyBatis-Plus 3.5.9,建议先跑一遍完整的 CRUD 集成测试再放行。因为 Spring Boot 3.4 之后对自动配置文件和配置属性的绑定方式做了调整,有些 starter 的配置项可能出现被忽略或警告的情况。实际上,MyBatis-Plus 3.5.9 之后官方也发布了更新版本,如果项目用了较新的 Spring Boot,考虑升级 MyBatis-Plus 到兼容版本会更省心。

另外一个容易被忽略的兼容性问题是Jakarta Validation 的版本。Spring Boot 3 默认使用jakarta.validation约束注解,javax.validation的注解不能用了。Controller 的@Validated和 DTO 里的@NotBlank、@NotNull如果导入了旧的 javax 包,运行时直接抛 NoClassDefFoundError。IDEA 自动导入有时会帮你选中旧坐标,写完代码后检查一下 import 语句。

7. 项目实践中的个人体会

这套技术栈在我实际项目里跑了半年多,说几点掏心窝子的体会。

Spring Boot 3 + MyBatis-Plus 3.5.9 的组合让后端 CRUD 开发确实提速不少。一个标准模块的开发流程可以压缩到:建表 → 建实体类 → 建 Mapper 接口 → 建 Service 类 → 建 Controller,全程不需要写任何 SQL,分页和排序也通过 Wrapper 快速完成。一个中等复杂度的管理后台,两个人两周就能把所有基础接口做完,这在过去 MyBatis 手写 SQL 的年代是不可想象的。

但这套框架也有它的“软肋”。MyBatis-Plus 的便利性会让一部分开发者在业务逻辑本已复杂时,仍然试图用 Wrapper 去解决一切问题,导致 SQL 的可读性和性能双双下降。我见过一个统计报表查询,用 LambdaQueryWrapper 拼了六七个条件、两个子查询,最后生成的 SQL 执行耗时三秒多。这种场景接手的人会很痛苦——Wrapper 不是 SQL,你没法像读 XML 那样直观地看到查询逻辑。所以我的原则很明确:能简单 Wrapper 解决的用 Wrapper,一旦查询边界触碰到多表 join、复杂子查询、动态排序,立即转到 XML。

我选型时最看重的其实是 MyBatis-Plus 背后那套约定优于配置的思想。它会替你规范表名、字段名、主键策略、逻辑删除,这些规范一旦被团队接受,代码风格会高度统一。与其说它是一个框架,不如说它是在帮你推进一套数据访问层的最佳实践。但前提是团队里得有人真正理解这些约定背后的原理,而不是只知道照着文档写。

最后分享一个小技巧:把 MyBatis-Plus 的 SQL 日志在本地开发时始终打开,养成看一眼执行 SQL 的习惯。很多隐蔽的问题,比如字段没走索引、比如多出了一条 count 查询、再比如逻辑删除没生效,看一眼日志就懂了。我在 review 同事代码时,第一步就是让他把相关接口的 SQL 日志贴出来,几句话就能说清问题。这个习惯,建议每个刚接触 MyBatis-Plus 的开发者尽早养成。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:02:56

PyTorch强化学习实战(27)——进化策略在强化学习中的应用

PyTorch强化学习实战&#xff08;27&#xff09;——进化策略在强化学习中的应用0. 前言1. 黑盒优化方法2. 进化策略3. 在 CartPole 环境中实现进化策略小结系列链接0. 前言 在本节中&#xff0c;我们将改变对强化学习 (Reinforcement Learning, RL) 训练的视角&#xff0c;转…

作者头像 李华
网站建设 2026/10/2 2:02:24

基于springboot + vue鲜花销售系统(源码+数据库+文档)

鲜花销售系统 目录 基于springboot vue鲜花销售系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue鲜花销售系统 一、前言 博主介绍&#xff1a;✌…

作者头像 李华
网站建设 2026/10/2 2:02:03

KCF与卡尔曼滤波融合:视觉目标跟踪的观测预测互补方案

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

作者头像 李华
网站建设 2026/10/2 2:02:03

MFC对话框添加工具栏:原理、代码与常见问题

简介&#xff1a;这是一份面向Visual C与MFC开发者的完整示例工程&#xff0c;聚焦对话框窗体如何集成工具栏这一常见需求&#xff0c;覆盖CDialog派生类搭建、工具栏资源设计、控件关联以及按钮消息映射等关键环节&#xff0c;适合正在学习MFC界面开发或需要为对话框添加快捷工…

作者头像 李华
网站建设 2026/10/2 2:01:14

RandAugment图像增强原理与工程落地实战指南

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

作者头像 李华