Mybatis-Plus 踩坑:check 关键字与 LambdaQueryWrapper 的救赎(数据库转译/更安全的映射关系获取)
数据库转译
什么是关键字转译?
转译(或“转义”)是一种保护机制。
简单来说,SQL 数据库(如 MySQL)有自己的“保留词汇”,例如 SELECT、CHECK、USER、ORDER、GROUP BY 等。当 SQL 解析器读到这些词时,会默认把它们当作命令或函数来执行。
当你“不巧”地将这些保留词(关键字)用作表名或字段名时,解析器就会“困惑”。
转译就是用特定的符号(例如反引号 ``)将这个词“包”起来,明确地告诉解析器:“嘿,别把它当命令,它只是一个名字!”
什么时候必须转译?
你会在两种主要情况下需要转译:
-
当标识符是“保留关键字”时
-
这是我们遇到的最主要的问题。
check是用于CHECK约束的关键字,user是一个内置函数/关键字。 -
例子:
SELECT check, user FROM buy;-> (失败) -
正确:
SELECT \check`, `user` FROM buy;` -> (成功)
-
-
当标识符不规范时
-
虽然极不推荐,但数据库允许你创建包含特殊字符、空格或小写字母的标识符(如果数据库本身大小写敏感)。
-
例子:
SELECT last name FROM my table;-> (失败) -
正确:
SELECT \last name` FROM `my table`;` -> (成功)
-
主流的数据库转译符号
常用的mysql一般使用反引号(``)做转译
Mybatis-Plus是如何处理的?
Mybatis-Plus (MP) 完美地集成了这一机制。它允许你在实体类 (DO) 中解决这个问题,而不是在业务代码中。
A. 针对字段(列名):@TableField
这就是我们的解决方案。通过 @TableField 注解,我们告诉 MP:“当你在 Java 中看到 noteUser 时,请在生成 SQL 时使用 \user`` ”。
// BuyDO.java
/**
* 核对状态[0:未核对|1:已核对]
*/
@TableField("`check`") //
private Boolean checkStatus;
/**
* 制单人
*/
@TableField("`user`") //
private String noteUser;
B. 针对表名:@TableName
同样的,如果你的表名是关键字(例如 order),你也可以转译:
// 假设表名叫 'order'
@TableName("`order`")
public class OrderDO { ... }
一个重要的区别:转译和防止SQL注入
请注意,我们讨论的标识符转译(转译列名/表名)与数据转译(防止 SQL 注入)是两回事。
-
标识符转译 (我们干的):
-
SELECT \check` FROM buy ...` -
目的: 解决 SQL 语法错误。
-
谁来做: 开发者在
BuyDO中手动添加注解。
-
-
数据转译 (Mybatis-Plus 干的):
-
... WHERE \user` = ?(?` 是占位符) -
目的: 防止 SQL 注入。
-
谁来做: Mybatis-Plus 和 JDBC 驱动程序自动处理。当你调用
queryWrapper.eq(BuyDO::getNoteUser, "admin")时,MP 会安全地将"admin"这个值放入?占位符,确保它只被当作字符串数据,而不是 SQL 命令。
-
问题排查过程
在 Spring Boot + Mybatis-Plus 开发中,实现一个简单的分页条件查询本应是信手拈来。但在这次“采购单列表”功能的开发中,我却连续遭遇了 Unknown column 和 SQLSyntaxErrorException 两个错误,并在修复代码后陷入了“重启-清理缓存-依然报错”的诡异循环。
以下记录了从错误发生、初步排查、错误升级、到最终通过 LambdaQueryWrapper 绕过缓存并解决问题的完整过程。
三次调试
第一次:Unknown column 'noteUser'
一切按部就班,Service 层代码如下:
// BuyServiceImpl.java (最初版本)
@Override
public PageDTO<PurchaseNoteListDTO> listPurchaseNote(PurchaseNoteQuery query){
Page<BuyDO> page = new Page<>(query.getPageIndex(), query.getPageSize());
QueryWrapper<BuyDO> queryWrapper = new QueryWrapper<>();
if (query.getNumber() != null && !query.getNumber().trim().isEmpty()) {
queryWrapper.like("number", query.getNumber());
}
if (query.getSupplier() != null && !query.getSupplier().trim().isEmpty()) {
queryWrapper.eq("supplier", query.getSupplier());
}
// 问题代码
if (query.getNoteUser() != null && !query.getNoteUser().trim().isEmpty()) {
queryWrapper.eq("noteUser", query.getNoteUser());
}
Page<BuyDO> resultPage = this.page(page, queryWrapper);
return PageDTO.create(resultPage, PurchaseNoteListDTO.class);
}
接口测试,第一个错误如期而至:
java.sql.SQLSyntaxErrorException: Unknown column 'noteUser' in 'where clause'
分析: 这是最常见的错误。我的 Java 实体 BuyDO.java 中使用了驼峰命名 noteUser,而数据库 buy 表中对应的字段是 user。
解决方案: 立刻修改 BuyDO.java,添加 @TableField 注解。
// BuyDO.java (第一次修改)
@TableField("user")
private String noteUser;
第二次:SQLSyntaxErrorException near 'check'
重启项目,再次测试。noteUser 的错误消失了,但一个全新的错误出现了:
java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax... near 'check AS checkStatus...'
分析: 这个错误指向了 SELECT 子句。Mybatis-Plus 在查询 BuyDO 时,试图将 check 列映射到 checkStatus 属性。
问题在于,check 是 MySQL 的保留关键字,不能直接用作列名。
解决方案: 数据库关键字转译。我们需要告诉 Mybatis-Plus,这个 check 是一个列名,而不是一个 SQL 命令。
修改 BuyDO.java,使用反引号 `` 来转义关键字:
// BuyDO.java (第二次修改)
// 之前映射 'user'
@TableField("user") // (很快我会发现 'user' 也是关键字)
private String noteUser;
// 新增映射 'check'
@TableField("`check`") // 使用反引号
private Boolean checkStatus;
第三次:Unknown column 'noteUser' 回归
我满以为解决了所有问题,重启项目...然而,我面对的竟然是第一个错误:
java.sql.SQLSyntaxErrorException: Unknown column 'noteUser' in 'where clause'
这太诡异了!我反复确认 BuyDO.java:
// BuyDO.java (最终正确版)
...
@TableField("`check`")
private Boolean checkStatus;
@TableField("`user`") // 我也把 'user' 加上了反引号,确保万无一失
private String noteUser;
...
我的 BuyDO.java 代码明明是正确的!
我尝试了所有能想到的办法:
-
重启 IDE。
-
使用 IDE 的
Rebuild Project。 -
Invalidate Caches / Restart。
但错误依旧。
最终诊断: 正在运行的 Spring Boot 应用死死地加载着一个旧的、没有正确注解的 BuyDO.class 文件。IDE 的构建缓存(或 Spring Boot DevTools 的热重载)在某个环节上“撒了谎”。
BuyServiceImpl.java 中 queryWrapper.eq("noteUser", ...) 这行代码,由于使用的是“魔术字符串” "noteUser",Mybatis-Plus 在运行时没有从新的 BuyDO.class 中加载到它对应的 @TableField("user") 注解。
最终的救赎:LambdaQueryWrapper
既然基于字符串的 QueryWrapper 无法获取最新的映射关系,我决定换一种方式——使用类型安全的 LambdaQueryWrapper。
修改 BuyServiceImpl.java:
// BuyServiceImpl.java (最终修复版)
@Override
public PageDTO<PurchaseNoteListDTO> listPurchaseNote(PurchaseNoteQuery query){
Page<BuyDO> page = new Page<>(query.getPageIndex(), query.getPageSize());
// 1. 切换到 LambdaQueryWrapper
LambdaQueryWrapper<BuyDO> queryWrapper = new LambdaQueryWrapper<>();
// 2. 使用方法引用 (Type-Safe)
if (query.getNumber() != null && !query.getNumber().trim().isEmpty()) {
queryWrapper.like(BuyDO::getNumber, query.getNumber());
}
if (query.getSupplier() != null && !query.getSupplier().trim().isEmpty()) {
queryWrapper.eq(BuyDO::getSupplier, query.getSupplier());
}
// 关键修复:
// Mybatis-Plus 现在会强制通过 BuyDO::getNoteUser
// 来找到 noteUser 字段上的 @TableField("`user`") 注解
if (query.getNoteUser() != null && !query.getNoteUser().trim().isEmpty()) {
queryWrapper.eq(BuyDO::getNoteUser, query.getNoteUser());
}
Page<BuyDO> resultPage = this.page(page, queryWrapper);
return PageDTO.create(resultPage, PurchaseNoteListDTO.class);
}
重启程序之后测试成功!
切换到 LambdaQueryWrapper 强制 Mybatis-Plus 在编译和运行时都必须依赖 BuyDO.java 的方法引用 BuyDO::getNoteUser。这个动作似乎终于“戳破”了 IDE 的顽固缓存,让 noteUser 字段上的 @TableField("user") 注解得以生效。
结语:
这次排错之旅虽然曲折,但也带来了深刻的教训:
-
数据库关键字转译: 永远不要低估
check、user、order、key这些常用词。在BuyDO.java中使用@TableField("列名")加反引号,是成本最低的保险。 -
缓存是万恶之源: 当你的代码看起来明明正确但运行就是报错时,十有八九是缓存问题。
mvn clean install或Build -> Rebuild Project是常规操作,但有时也不可靠。 -
拥抱
LambdaQueryWrapper: 永远优先使用LambdaQueryWrapper。它不仅能避免“魔术字符串”带来的拼写错误,还能在编译期锁定类型,更重要的是,在这次经历中,它成了我绕过 IDE 缓存、解决灵异问题的“银弹”。
希望这篇博客能帮助到遇到类似问题的你。
更多推荐


所有评论(0)