第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 层方法时统一检查更新受影响行数。


相关推荐
覆东流10 小时前
7.Java数组
java·开发语言·后端
threerocks11 小时前
AI 原生软件开发生命周期手册 - 如何借助 AI,逐阶段改造软件开发生命周期
前端·javascript·后端
JavaPub-rodert11 小时前
王仕宇在 Go Context 如何解决协程泄漏与超时控制
开发语言·后端·golang·iphone·javapub·王仕宇
深漂的华哥11 小时前
Ruoyi-Plus前后端分离场景下,数据加密传输
java·spring boot·后端·开源·maven·ruoyi
why技术13 小时前
eli5,我觉得这个全网在吹的技能,使用体验真的很一般啊。
前端·人工智能·后端
学习星球13 小时前
Qwik 框架入门实战:从开源项目 Qwik City 开始,用可恢复性替代水合
后端·前端框架·开源·c5全栈
IT_陈寒15 小时前
被Java的final坑惨了,这些细节你可能也忽略了
前端·人工智能·后端
Zadig15 小时前
企业 Agent 总烂在 Demo 里?Zadig 工作流 AI 任务给了一条路
后端·devops
Zadig15 小时前
告别"人肉扛雷":Zadig 用 AI 接管发布前最脏最累的 15 分钟
后端·aiops
Nturmoils16 小时前
向量数据库不该成为新孤岛:KingbaseES 多模融合架构如何减少数据搬运
后端