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

14 阅读3分钟

第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 加载过程:

logging:
  level:
    org.mybatis: debug
    org.apache.ibatis: debug

22.2 TooManyResultsException

@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,直接在数据库客户端里重放

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

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

22.4 MyBatis-Plus 特有异常

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