news 2026/9/8 9:24:32

MyBatisPlus生产环境配置避坑指南:分页失效与逻辑删除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatisPlus生产环境配置避坑指南:分页失效与逻辑删除

周一早上刚坐到工位,群里就有人甩了个截图:分页接口返回的 total 永远是 0,可 List 里明明有数据。下一秒又有人补了一句:订单表建了唯一索引,逻辑删除过的单据再插入,直接 Duplicate entry 卡死。老实说,MyBatisPlus 用好了是效率神器,用不好就是连环坑。这两年我接手过好几个 SpringBoot + MyBatisPlus 项目,几乎每个项目都会在同样的地方翻车:分页拦截器没生效、selectCount 查出来是 0、字段名撞了数据库关键字、批量插入慢得离谱。这篇文章就把我在生产环境里排查过、处理过的问题系统整理一遍,重点讲配置层的细节、坑在哪里,以及每个坑背后的原理,顺便给出一套可以直接抄作业的配置方案。

写这篇文章之前我也习惯性地翻了一下最近的搜索趋势,发现大量开发者都在搜“mybatisplus 单页500条限制”“mybatisplus selectcount 为0”“mybatisplus分页失效”“mybatisplus关键字”这类问题,另外还有一堆是 JDK、Maven、MySQL 安装配置。确实,很多项目第一关就卡在环境搭建,但真正让团队头疼的从来不是环境,而是这些配置细节和隐性问题。所以这篇文章主要面向正在用 MyBatisPlus 做业务系统、尤其是准备上生产环境的团队,也适合刚把 MyBatis 项目迁到 MyBatisPlus 的开发者。我会把配置项掰开揉碎讲清楚,并且把网上那些搜不到答案的“为什么”一并补上。

1. MyBatisPlus 生产就绪,先要把定位和版本搞明白

1.1 先看清 MyBatisPlus 的定位:增强工具,不是新型框架

我遇到过不少新人上来就问:MyBatisPlus 是不是要把 MyBatis 替换掉?其实不是。MyBatisPlus 是在 MyBatis 之上做增强的,底层仍然走 MyBatis 的 SqlSession 机制,SQL 解析、会话管理、事务这些核心能力一点都没变。它做的只是把单表 CRUD、分页、逻辑删除、自动填充、乐观锁这些高频重复操作封装成了现成能力,开发者不需要再为每个实体写一套 XML。

这也是"生产就绪"的第一个判断标准:你的团队是主要做单表操作,还是大量复杂多表 JOIN。如果是前者,MyBatisPlus 能省下大量样板代码;如果是后者,最好还是老老实实手写 XML,MyBatisPlus 的自定义 SQL 能力跟 MyBatis 完全一致,不存在冲突,两者是配合关系,不是二选一。

实际项目中我见过最舒服的分工是:单表查询、简单的列表分页、字典表操作全走 MyBatisPlus 的 BaseMapper;报表类、多表关联、复杂条件动态 SQL 走自定义 XML。这样既保证了开发速度,也不至于让 XML 文件膨胀到没法维护。

1.2 版本选型和生产依赖配置

版本问题看起来不起眼,但踩坑率极高。我用过一个项目,SpringBoot 2.7 配了 MyBatisPlus 3.5.2,分页和乐观锁都能正常工作。但另一个项目升级到 SpringBoot 3.2 后发现,原来那套 mybatis-plus-boot-starter 直接启动报错,原因是 SpringBoot 3 改用了 Jakarta EE,旧版本的 MP 初始化逻辑无法兼容。后来换成mybatis-plus-spring-boot3-starter才解决。

如果你是全新项目,建议直接用一个相对新的稳定版本,比如 MyBatisPlus 3.5.3 以上,SpringBoot 2.x 用mybatis-plus-boot-starter,SpringBoot 3.x 用mybatis-plus-spring-boot3-starter。注意不要自己随意混搭版本,MP 的拦截器机制和 MyBatis 核心版本耦合很深,换版本前先看官方版本兼容矩阵。

生产环境里,我一般还会关闭 MP 启动时打印的 Banner,说实话那东西除了占日志空间没有任何用处:

mybatis-plus: global-config: banner: false

1.3 一份可以直接复制的基础配置

下面这份是我在多个生产项目里验证过的核心配置,逐行解释一下作用:

mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl global-config: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

mapper-locations指定 XML 文件位置,如果你有自定义 SQL,一定要把路径配对,否则启动后调用自定义 Mapper 方法会报Invalid bound statementtype-aliases-package是实体类扫描路径,配了之后 XML 里写 resultType 可以直接用类名,不用带全限定名。map-underscore-to-camel-case是驼峰映射开关,数据库字段create_time会自动映射到实体属性createTime,这个强烈建议开启,否则每次查询都要手动写@TableField

id-type我一般选assign_id,也就是 MP 默认的雪花 ID。生产环境强烈不建议用数据库自增主键做分布式系统的主键,ID 冲突和数据迁移都是大麻烦。雪花 ID 虽然看着长,但全局唯一、有序性也够用。

2. 配置环节最容易埋雷的几个点

2.1 分页拦截器配置:所谓“500 条限制”到底是怎么回事

先回答很多人在搜的那个问题——"MyBatisPlus 单页 500 条限制"。其实 MyBatisPlus 本身并没有内置一个写死的 500 条上限,这个数字通常是两种情况造成的:一是分页插件里有人主动设置了maxLimit=500,二是前端分页组件默认每页 500 条。但很多团队搜到这个说法后,根本不知道还有maxLimit这个参数,于是分页明明配好了,查出来的数据却永远只有 500 条,最后怪到框架头上。

我建议所有生产项目必须显式设置maxLimit,这是一种自我保护机制。想象一下,如果有人写了个接口,前端只传了一个巨大的pageSize,比如 100000,那这条 SQL 会直接把数据库内存打爆。设置一个上限,超过就抛异常或者按上限返回,这是最便宜的限流手段。

分页拦截器还有一个隐藏参数叫overflow,默认 false。含义是:当请求页码超过总页数时,是返回空数据还是自动修正到最后一页。真实场景里,用户停留在列表第 10 页,管理员删了几条数据,用户再点下一页,如果 overflow=false,就会看到空白页,体验很差。我通常把 overflow 设成 true,宁可多查一次,也别让用户一脸懵。

配置方式如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); pagination.setOverflow(true); interceptor.addInnerInterceptor(pagination); return interceptor; } }

注意,这里的DbType一定要跟实际数据库匹配,MySQL、PostgreSQL、Oracle 的分页语法完全不同,MP 是根据 DbType 来生成对应方言的分页 SQL 的。配错了,轻则分页结果不对,重则直接 SQL 报错。

2.2 selectCount 返回 0 的完整排查路径

“列表有数据,但 total 是 0”,这个问题的出现频率高得惊人。MyBatisPlus 的selectCount方法在底层会自动生成SELECT COUNT(*)的 SQL,并且在原有查询条件基础上追加逻辑删除条件。所以当你用selectCount查出来是 0,第一步要做的不是看代码,而是把实际执行的 SQL 打出来看。

排查路径我一般按顺序走:

第一,看 SQL 里的条件。打印出来的 count SQL 是不是多了deleted=0?如果这张表本身不需要逻辑删除,但实体类继承了某个带@TableLogic的基类,就会多出这个条件,把已删除的数据全部过滤掉,计数自然少了。

第二,看条件构造器的拼接逻辑。很多人用LambdaQueryWrapper时,会把eqlikeandor混在一起,最后生成的 SQL 条件优先级完全不对。比如eq("status", 1).or().eq("type", 2).eq("deleted", 0)生成出来的 where 条件可能把deleted=0只作用在type=2上。排查方法很简单,打开 SQL 日志看括号位置。

第三,看字段映射。如果实体属性叫isDeleted,数据库列叫is_deleted,而项目又没开启驼峰映射,那查询条件可能生成为is_deleted = ?,这种情况在 MySQL 里有时不会立刻报错,但查出来的结果必然不对。

第四,检查事务隔离级别。如果是“刚插入数据就 count 查不到”,那可能是你的 Service 方法没有加事务,或者事务隔离级别是READ_COMMITTED,另一个连接还没提交,当前连接自然看不到。这种不属于 MyBatisPlus 问题,但经常被误认为是 MP 的 bug,我也见过好几次。

2.3 关键字和保留字:order、rank、desc 这些字段名的定时炸弹

很多团队的数据库表字段喜欢用orderdesclevelrankintervalyear这类词,MySQL 5.7 时代还能勉强跑,升级到 MySQL 8.0 后就会开始报 SQLSyntaxErrorException,因为 8.0 对保留字的检查更严格了。

比如有个需求要给订单排序,字段名直接叫order,MyBatisPlus 生成的 SQL 是:

SELECT id, order FROM t_order

MySQL 8.0 直接报语法错误。解决办法有三种:

第一种,单字段转义,在实体上用@TableField指引用符号包起来:

@TableField("`order`") private String order;

第二种,全局配置column-format,让 MP 生成 SQL 时为所有列名都加上反引号:

mybatis-plus: global-config: db-config: column-format: "`%s`"

但这招我不推荐,因为开了之后所有字段都被包上反引号,日志阅读性变差,而且在 PostgreSQL、SQLServer 上反引号语法不通用,换库就完蛋。

第三种最推荐:数据库设计阶段就约定好不用保留字,已经上线的表就老老实实改字段名。我知道改字段名有风险,但这是长远最省事的方案。如果实在不能改,那就给那少数几个字段单独加@TableField转义,不要全局开启。

2.4 驼峰映射与字段命名规范

map-underscore-to-camel-case是我见过配置率最高、但讨论最少的一个开关。开启之后,MP 会自动把user_name映射为userName,省掉大量@TableField注解。但这里有个隐蔽的坑:实体属性如果叫uName,数据库列叫user_name,驼峰映射规则是“去下划线后首字母小写”,它生成的是userName,不是uName,结果就是查询能查出列,但映射不到实体属性上,最后属性全是 null。

另一个容易忽略的是数据库列本身就有大写的情况,比如userId这种驼峰式列名,映射规则也容易出问题。我一般直接约定:数据库列一律小写下划线,实体属性一律小驼峰,从源头避免这类问题。

如果你用了 MyBatis 原生的@Results或 XML 里的resultMap,要注意这些映射的优先级高于全局驼峰配置。XML 里一旦手写了 resultMap,MP 的自动驼峰映射就不会发挥作用,必须自己把每个字段对应关系写全,否则又是 null。

3. 生产环境高频故障实录:分页失效、逻辑删除、乐观锁

3.1 分页失效的三种典型场景

分页失效是最经典的“搜不到答案”系列。配置看着没问题,但一执行,MP 还是查出了全表数据。根据我的排查经验,最常见的场景有三个。

第一个场景,压根没注册分页拦截器。很多人以为引入mybatis-plus-boot-starter就自带分页能力了,其实不是。分页插件必须通过MybatisPlusInterceptor手动注册,否则Page参数传给 Mapper,MP 会把它当成普通参数忽略掉,SQL 执行时页面大小完全不生效,返回所有数据。这个坑几乎每个新项目都会踩一次。

第二个场景,分页的IPage对象没有作为 Mapper 方法第一个参数。MP 的分页拦截器是靠识别 Mapper 方法参数里的IPage来工作的,如果方法签名长这样:

List<User> selectUserList(User user, Page<User> page);

理论上也能生效,因为 MP 会扫描所有参数找到 IPage。但我见过有人把 Page 放在自定义对象里,比如req.setPage(new Page<>(1,10)),这种就无法被识别,结果自然全量返回。规范做法是把 Page 作为 Mapper 方法的第一个参数,简洁又保险。

第三个场景,多表 JOIN 查询时 count 生成错误。分页拦截器在拦截到分页查询后,会自动把原 SQL 改写为 count 查询。单表没问题,但多表 JOIN、带DISTINCT、带GROUP BY的 SQL,改写后的 count 语句经常在语法层面就出错,或者生成的 count 对不上。

因为 JOIN 场景下,如果没有明确分页主表,拦截器生成的 count 会对整个 JOIN 结果做统计,一旦 JOIN 产生笛卡尔积,count 数值就是错的。这时候最靠谱的方案是手动指定 count SQL,使用Page.setCountId或者给这个 Mapper 方法单独写一个 count 方法,让分页插件去调用你精确计算过的 SQL。

另外,分页 SQL 中如果有ORDER BY,MySQL 5.7 和 8.0 在执行 count 优化时的行为还不太一样,这也是为什么线上 MySQL 版本升级后分页反馈异常的常见原因之一。

3.2 @TableLogic 的连带效应:唯一索引、count、自定义 XML

逻辑删除是 MyBatisPlus 最受欢迎的功能之一,也是生产环境最容易出问题的功能。它帮你做的只是在delete操作时把DELETE语句改成UPDATE deleted=1,在查询时自动追加AND deleted=0

问题来了:如果你在业务表上建了唯一索引,比如订单表用order_no建唯一索引,那么逻辑删除一条订单后,这条记录还留在表里,order_no还被占用着。下次新增同号订单,直接 Duplicate entry。这是我在一个日单量不小的系统里真实遇到过的场景,当时上线才一周就有用户反馈重复下单失败。

解决方案无非两个:一是把唯一索引改成联合索引,把deleted字段也放进去,这样已删除的记录deleted=1,新插入的记录deleted=0,唯一性就能区分开;二是如果业务上确实允许复用被删的编号,那就需要物理删除或者用不带逻辑删除的 Mapper 方法。

还有一个隐蔽点:你在 XML 里手写 SQL 时,如果写的是SELECT * FROM t_order WHERE user_id = #{userId},MP 的自动逻辑删除条件不会生效,因为拦截器只处理 MP 生成的通用方法,不处理自定义 XML。也就是说,同一个实体,用 BaseMapper 自带方法查出来的数据是过滤掉已删除的,但手写 XML 查出来的却含已删除数据。这个不一致很容易在生产环境里造成账单类数据重复,排查时一定要记得检查自定义 SQL 里有没有手动补上and deleted = 0

3.3 自动填充字段:create_time、update_time 的坑

MetaObjectHandler是 MP 用来做公共字段自动填充的入口,比如创建时间、修改时间、创建人、更新人。很多团队的实现方式是在实体类上标注@TableField(fill = FieldFill.INSERT)@TableField(fill = FieldFill.INSERT_UPDATE),然后写一个 Handler。这个方案本身没问题,代码看着也清爽,但生产环境里坑就藏在时间格式和时区里。

我遇到过最典型的一个问题:数据库字段是datetime,实体属性是LocalDateTime,Handler 里填充的却是new Date()。看起来能存进去,但 MP 映射时出了隐式转换问题,最终查出来的时间和实际相差 8 小时,或者部分记录的时间字段变成 null。排查到最后发现是 Handler 里统一用LocalDateTime.now()才对。

另一个坑是FieldFill.UPDATE只对updateById生效,对UpdateWrapper方式不生效。比如你写:

userService.update(new User(), new LambdaUpdateWrapper<User>() .eq(User::getId, 1) .set(User::getName, "test"));

这种写法基于update(entity, wrapper),Handler 不会自动填充 updateTime。正确做法是显式在set里把 updateTime 带上,或者尽量用updateById

还有一个地方容易漏:大字段更新时,如果实体属性有@TableField(updateStrategy = FieldStrategy.NOT_NULL)这类的策略配置,自动填充可能被覆盖或者跳过。遇到这类问题,直接看执行 SQL 最直观,别猜。

3.4 乐观锁与批量写的性能问题

乐观锁在 MP 里的实现方式很优雅:实体类加一个@Version注解的字段,再注册OptimisticLockerInnerInterceptor。每次 update 时 MP 会自动在 SQL 里带上version = 当前版本,并且更新后version = version + 1。这个机制本身很成熟,但现场问题也不少。

最常见的是忘了注册乐观锁拦截器。有@Version字段没有拦截器,MP 不会帮助你做版本校验,update 语句就跟普通 update 一样,乐观锁形同虚设。这个问题隐蔽在"代码看什么都是对的,运行结果就是不对"。

另外,乐观锁字段在实体类上是基本类型int时,需要给默认值 1。如果是Integer且为 null,第一次 update 时生成的 SQL 会变成where version is null,不仅没起到乐观锁作用,还可能更新错行。这个我在数据修复脚本里踩过一次,教训深刻。

再说性能。批量插入是很多团队迁到 MyBatisPlus 后最想开的功能,saveBatch确实用起来很爽,但如果你没在 JDBC URL 里加上rewriteBatchedStatements=true,这玩意儿就是伪批处理。MySQL 默认不会重写批量语句,MP 的saveBatch生成的批量插入 SQL 到了 MySQL 驱动层可能被逐条执行,性能提升约等于零。加上这个参数后,MySQL 会把多条 insert 合并成一条多值 insert 执行,单批插入几千条数据的耗时能从十几秒降到一两秒。

JDBC URL 配置:

jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true

如果你一边用批处理、一边又在代码里循环调用saveinsert,那不管怎么调优都没用,批处理和循环单条是两回事。要批量就一口气saveBatch,要单条就单条,别混着来。

4. 生产环境配置建议:连接池、SQL 监控与扩展点取舍

4.1 Hikari 连接池参数怎么调才合理

SpringBoot 2.x 默认集成 HikariCP,这也是现在 Java 界综合性能最好的连接池。大部分团队用默认配置跑起来没问题,但生产环境我建议至少明确设置几个参数,别完全依赖默认值。

maximum-pool-size默认是 10,很多人以为越大越好,直接调到 200,结果数据库连接数被打满,响应反而变慢。连接不是越多越好,每个连接背后都有数据库端的内存和线程资源,连接数过多会让数据库频繁切换上下文。经验法则:一个 4 核 8G 的 MySQL 实例上,应用连接池上限设在 20 到 50 之间比较稳妥,具体要压测验证。

connection-timeout默认 30000 毫秒,警示意义大于实际意义。如果连接池耗尽,请求要卡 30 秒才会抛异常。对在线接口来说这个时间太长了,我一般会调到 3000 到 5000 毫秒,宁可快速失败让上游感知,也不要让用户傻等 30 秒。

Hikari 还有几个隐蔽参数,比如maximum-lifetime要小于数据库的wait_timeout,否则连接会被数据库服务端回收,但连接池不知道,拿到手才发现连接已经失效。MySQL 默认wait_timeout是 8 小时,Hikari 的maximum-lifetime默认 30 分钟,所以一般不会撞上,但如果数据库运维改了 wait_timeout,连接池也对应调整。

4.2 慢 SQL 和日志打印的定位技巧

MyBatisPlus 提供了PerformanceInterceptor这类插件来打印执行耗时,但我不建议生产环境长期开启它,那个插件在高并发下会有额外的性能损耗和日志刷屏。更合理的做法是直接用 MySQL 官方的慢查询日志:

slow_query_log=ON long_query_time=1 slow_query_log_file=/var/log/mysql/slow.log

然后把long_query_time设成 1 秒,对线上区分度很高。再配合EXPLAIN看执行计划,基本能定位绝大多数 SQL 性能问题。

如果你在项目里想快速看 SQL 执行耗时,又不想引入太多依赖,可以在 Mybatis 配置里把日志级别调成 DEBUG,但只对某个 mapper 包单独开,避免全局刷屏。比如在application.yml里:

logging: level: com.example.demo.mapper: debug

这样只打印 Mapper 接口包下的 SQL,日常开发排错够用,又不至于被日志淹没。

4.3 多租户、逻辑删除、乐观锁这些内置扩展,要不要全上

MyBatisPlus 的扩展能力很强,有TenantLineInnerInterceptor做多租户、BlockAttackInnerInterceptor防止全表更新删除、IllegalSQLInnerInterceptor做 SQL 规范性校验。看起来功能很全,但我的建议是:按需引入,不搞大而全。

多租户插件是这里面使用成本最高的一个。它会在你所有查询上强制追加租户条件,如果某个查询需要跨租户或者查全量数据,就得在代码里手动拼租户条件或写自定义 SQL 绕过。业务没想清楚之前贸然开启,后面每个查询都要照顾这个隐藏条件,非常痛苦。我见过一个项目因为多租户插件和分页插件顺序配错,导致分页 count 查询带上重复的租户条件,排查了整整一天。

如果团队目前只是单租户模式,真的不必提前引入这个复杂度。同样,BlockAttackInterceptor这种安全插件可以配置,但也要评估业务里是否有合法的全表更新需求,否则上线后可能把内部定时任务给误杀。

我的原则是:生产环境默认只开分页和乐观锁两个拦截器,逻辑删除按表按需配置,其他扩展等业务真正需要时再加,避免为未来可能用不到的复杂度买单。

4.4 单元测试与回归保障

MyBatisPlus 的 CRUD 方法封装度很高,很多代码不需要写 XML,但也正因为如此,一些隐藏行为靠代码评审根本发现不了。我强烈建议给 Mapper 层建立一套轻量级的单元测试,至少覆盖以下几个场景:新增后能按主键查到;updateById后修改字段生效;逻辑删除后列表查询不再返回;分页查询 total 值和列表数量一致;自定义 XML 方法能正常返回结果。

最简单的做法是用 H2 内存数据库配合 MP 的 schema 脚本做 Mapper 层测试,不需要依赖真实 MySQL,本地跑起来秒级完成。数据源可以在测试配置里覆盖:

spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE

这样每次提交代码前跑一遍,很多"看起来对但一跑就炸"的问题能在测试阶段暴露,而不是上线后让报警群来提醒你。

5. 常见问题速查表与一套标准排查动作

5.1 高频问题速查表

我把这些年遇到的高频问题整理成一张表格,方便你按图索骥。实际排查时,先对号入座,再看对应章节的详细说明。

症状可能原因快速定位方向
分页查询返回全部数据分页拦截器未注册检查 MybatisPlusInterceptor 是否有 PaginationInnerInterceptor
total 为 0 但列表有数据逻辑删除条件/BETWEEN 条件/字段映射打印实际执行的 count SQL
分页数据始终只有 500 条分页插件设置了 maxLimit=500检查 setMaxLimit 配置,按业务调整
查询报 Unknown column字段名是数据库关键字检查 order、desc、rank 等保留字字段
逻辑删除后再插入报唯一键冲突唯一索引未包含 deleted 字段改成联合唯一索引,或改为物理删除
updateById 修改的新值没生效字段为空且策略为 NOT_NULL使用 UpdateWrapper.set 或者调整字段策略
乐观锁总是更新失败拦截器未注册或版本字段未初始化检查 OptimisticLockerInnerInterceptor 和默认值
saveBatch 批量插入很慢未开启 rewriteBatchedStatementsJDBC URL 拼接该参数
查询列表有数据但字段全是 null驼峰映射未开启或 resultMap 手动覆盖开启 map-underscore-to-camel-case
手写 XML 查出已逻辑删除的数据自定义 SQL 未追加 deleted=0XML 条件里手动补逻辑删除条件
查询条件覆盖了全表导致错更新LambdaUpdateWrapper 传了空值用三参 eq(condition, column, val) 防止空条件拼接

5.2 一套标准排查动作清单

遇到 MyBatisPlus 相关问题时,我建议按下面的顺序排查,不要一上来就怀疑框架有 bug,八成是配置或者用法问题。

第一步,打开 SQL 日志,把 MP 实际执行的 SQL 完整打出来。用logging.level.你的mapper包: debug的方式,最直接地看到 MP 生成了什么 SQL,很多问题看到 SQL 的一瞬间就明白了。

第二步,检查拦截器顺序。多个 InnerInterceptor 在MybatisPlusInterceptor中是有顺序的,一般分页在前、乐观锁在后,顺序配错可能导致某些拦截器不生效,或者执行了异常逻辑。不确定的时候,可以先把代码简化到只留分页拦截器,再逐步加回来。

第三步,检查实体类注解与数据库表结构的对应关系。把每个实体的@TableName@TableField@TableLogic@Version逐一对照表结构,重点关注字段名是否撞保留字、逻辑删除字段是否有默认值、版本字段是否为空时会影响更新。

第四步,确认版本兼容性。包括 MyBatisPlus 和 SpringBoot 的版本、MyBatisPlus 和 MyBatis 的版本、JDBC 驱动的版本。这三个维度任何一个不匹配,都可能引发诡异问题。

第五步,回归测试。修改完配置或者代码后,至少把分页、新增、修改、删除、逻辑删除、乐观锁这几个场景各跑一遍,别只验证报错的单点。

在线上排查这类问题的时候,我还习惯把责任边界划分清楚:是 MyBatisPlus 生成的 SQL 不对,还是业务代码条件构造不对,还是数据库结构不对。三种情况的处理方式完全不同。先把 SQL 打出来,一眼就能看出该找谁。这也是为什么我一直强调,排查工具再多,都不如一份清晰的 SQL 日志来得实在。

最后说一个我个人的体会:MyBatisPlus 这类框架给开发带来很大便利,但它从来不是银弹。越是用得顺手,越要弄清楚生成的 SQL 长什么样。很多生产事故不是因为框架能力不行,而是开发人员把框架当成了黑盒,出了问题才去翻日志。如果你能把 MP 的执行链路理解清楚,把分页、逻辑删除、乐观锁这几个核心机制的原理吃透,大多数所谓“坑”都能在设计阶段避开。真等到报警响起来再排查,成本至少是写代码时的十倍。

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

智能文档处理工具Copilot:从环境配置到批量解析实战

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

作者头像 李华
网站建设 2026/9/8 9:23:58

AI Agent实战:30个Skill+8个岗位,打造高效虚拟团队

1. 为什么给AI装30多个Skill&#xff0c;还要排成8个岗位 先说个挺反常识的现象&#xff1a;很多人觉得AI Agent能力不够&#xff0c;是因为模型不够聪明。但我装了30多个Skill、给AI排完8个岗位之后&#xff0c;最大的感受是——模型能力只是地基&#xff0c;真正的差距在于你…

作者头像 李华
网站建设 2026/9/8 9:23:21

不关闭不修改不读写内存:ACE反作弊进程资源占用优化方案

ACE 反作弊进程占用高&#xff0c;一直是不少 PC 玩家的痛点&#xff1a;游戏明明已经关闭&#xff0c;后台的 ACE 组件还在吃 CPU 和内存&#xff1b;游戏运行中&#xff0c;它又时不时抢占前台资源&#xff0c;造成掉帧和卡顿。更麻烦的是&#xff0c;如果直接卸载、禁用&…

作者头像 李华
网站建设 2026/9/8 9:23:20

用AI高效阅读鸿蒙源码仓库:从入门到精通的实战指南

简介&#xff1a;面向鸿蒙开发者的阅读3.0鸿蒙版仓库资源&#xff0c;聚焦鸿蒙OS下阅读类APP的适配与功能扩展&#xff0c;适合具备ArkTS基础、希望深入阅读器实现的开发者。压缩包内含886个文件&#xff0c;约5.58MB&#xff0c;其中ets源码334个用于页面逻辑&#xff0c;svg与…

作者头像 李华
网站建设 2026/9/8 9:22:00

ESP32上电不启动?Strapping引脚才是真凶——从原理到实战排查指南

干这行最怕遇到一种问题&#xff1a;芯片、代码、Flash 看起来全都没毛病&#xff0c;但板子一上电就是跑不起来。之前我调过一块自制的 ESP32 板子&#xff0c;上电后串口死活没输出&#xff0c;换芯片、换 Flash、重新焊晶振都试过&#xff0c;折腾了两天才发现&#xff0c;问…

作者头像 李华
网站建设 2026/9/8 9:21:47

MCP协议实战:从零搭建AI驱动Unity与Unreal的完整工具链

先说明一下&#xff1a;这篇内容我不做任何铺垫&#xff0c;直接上干货。2026年了&#xff0c;AI写代码早就不是什么新鲜事&#xff0c;真正让游戏开发者兴奋的&#xff0c;是AI开始能"亲手操作"游戏引擎了——从Unity场景里批量摆物件&#xff0c;到Unreal编辑器里自…

作者头像 李华