第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试

第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试

22.1 BindingException: Invalid bound statement (not found)

根本原因排查清单(按命中概率排序):

  1. Mapper XML 文件没有被扫描到 :检查 mybatis.mapper-locations 配置的路径是否真的覆盖到了 XML 文件所在目录(尤其常见于 Maven 多模块项目,XML 放在 src/main/resources 下但 mapper-locations 只配置了主模块路径,遗漏了子模块)。
  2. XML 的 namespace 和 Mapper 接口的全限定类名不一致:哪怕只是大小写不同或者包路径打错一个字符,都会导致找不到绑定关系。
  3. 方法名在接口和 XML 里不一致<select id="selectById"> 必须和接口方法名 selectById 完全一致(区分大小写)。
  4. 注解方式(@Select 等)和 XML 方式混用时配置冲突:同一个方法同时有 XML 定义和注解定义,会导致绑定行为不确定。

排查手段: 开启 MyBatis 的 debug 日志,观察启动阶段的 Mapper 加载过程:

yaml 复制代码
logging:
  level:
    org.mybatis: debug
    org.apache.ibatis: debug

22.2 TooManyResultsException

java 复制代码
@Select("SELECT * FROM user WHERE status = #{status}")
User selectOne(String status); // 如果 status 对应多条记录,抛 TooManyResultsException

根本原因: selectOne 语义上约定"最多返回一条结果",MyBatis 在拿到结果集后如果发现行数 > 1,主动抛出这个异常(而不是悄悄只返回第一条,掩盖潜在的数据问题)。

解决方案: 如果业务上确实可能返回多条,改用 List<User> selectList(...);如果业务上"理论上只应该有一条"但抛出了这个异常,说明数据本身出现了不符合预期的重复,应该去排查数据层面的问题(是否遗漏了唯一索引约束),而不是简单地把返回类型改成 List 掩盖过去。

22.3 SQL 调试实战:从"看不懂 MyBatis 报错"到"看到真实执行的 SQL"

MyBatis 抛出的 PersistenceException 往往会包一层 Cause: java.sql.SQLSyntaxErrorException 之类的底层 JDBC 异常,直接看 MyBatis 层的报错经常不知所措,最有效的排查方式是拿到真正拼接执行的 SQL,直接在数据库客户端里重放

yaml 复制代码
# 方式1:开启对应 Mapper 包的 debug 日志,会打印出实际执行的 SQL 和参数(分两行打印,需要手动拼接)
logging:
  level:
    com.example.mapper: debug
sql 复制代码
==>  Preparing: SELECT * FROM user WHERE id = ? AND status = ?
==> Parameters: 1(Integer), ACTIVE(String)
xml 复制代码
<!-- 方式2:MyBatis-Plus 提供的 p6spy / druid 的 filters=stat 也能拿到完整可执行 SQL(含参数已经替换进去,不需要手动拼接) -->

核心原则:MyBatis/JDBC 层面的报错,第一步永远是先拿到"真正被数据库执行的那条完整 SQL",再拿这条 SQL 直接到数据库客户端(Navicat/DataGrip/命令行)里执行验证,绝大多数问题(字段名拼写错误、类型不匹配、关联表写错)在数据库客户端直接执行时都会给出比 MyBatis 包装后的异常更直接的报错信息。

22.4 MyBatis-Plus 特有异常

java 复制代码
// MybatisPlusException: 多次调用 lambda 表达式条件构造器时字段解析失败
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(User::getName, "test"); // 如果 User 类没有对应字段的 getter 方法引用能正确解析,可能抛异常

// 乐观锁更新失败(不直接抛异常,但需要注意返回值)
int rows = userMapper.updateById(user); // 如果 @Version 字段值和数据库当前值不一致,rows 返回 0 而非抛异常
if (rows == 0) {
    throw new OptimisticLockException("数据已被其他事务修改,请刷新后重试");
}

乐观锁场景是一个容易被忽视的"隐性异常" :MyBatis-Plus 的乐观锁插件在版本冲突时不会主动抛异常 ,只是让 update 语句实际影响 0 行,如果业务代码没有检查返回的受影响行数,会误以为更新成功,实际上数据完全没有被修改------这是一类"该抛异常但没抛"导致业务逻辑静默出错的典型场景,需要在封装 Service 层方法时统一检查更新受影响行数。


相关推荐
考虑考虑6 小时前
数据库中的EXISTS
运维·数据库·后端
郑州光合科技余经理7 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
毅炼7 小时前
Rover-Suite 开源发布:轻量服务注册中心与网关一体化方案
java·后端·系统架构·gateway
苏三说技术12 小时前
为什么越来越多人用OpenWiki?
后端
IT_陈寒12 小时前
Java里用Stream.parallel()翻车实录,这性能还不如单线程
前端·人工智能·后端
Bs_MoneyMagnet12 小时前
基于springboot+vue的生态果园采摘预约系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·毕业设计·计算机毕业设计
码事漫谈12 小时前
FDE 的起源、形态与真实边界
后端
ServBay12 小时前
Grok Bot 还是 OpenClaw?聊聊真正能替人打杂的 AI 智能体
后端·aigc·grok
掘金挖土13 小时前
前端手摸手跑路之 AI 应用开发(五)
前端·后端
弈栈录13 小时前
LangGraph 入门:用状态机设计可靠的 Agent 工作流
后端·程序员