第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试
22.1 BindingException: Invalid bound statement (not found)
根本原因排查清单(按命中概率排序):
- Mapper XML 文件没有被扫描到 :检查
mybatis.mapper-locations配置的路径是否真的覆盖到了 XML 文件所在目录(尤其常见于 Maven 多模块项目,XML 放在src/main/resources下但mapper-locations只配置了主模块路径,遗漏了子模块)。 - XML 的
namespace和 Mapper 接口的全限定类名不一致:哪怕只是大小写不同或者包路径打错一个字符,都会导致找不到绑定关系。 - 方法名在接口和 XML 里不一致 :
<select id="selectById">必须和接口方法名selectById完全一致(区分大小写)。 - 注解方式(
@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 层方法时统一检查更新受影响行数。