数据库转译

什么是关键字转译?

转译(或“转义”)是一种保护机制。

简单来说,SQL 数据库(如 MySQL)有自己的“保留词汇”,例如 SELECTCHECKUSERORDERGROUP BY 等。当 SQL 解析器读到这些词时,会默认把它们当作命令函数来执行。

当你“不巧”地将这些保留词(关键字)用作表名字段名时,解析器就会“困惑”。

转译就是用特定的符号(例如反引号 ``)将这个词“包”起来,明确地告诉解析器:“嘿,别把它当命令,它只是一个名字!

什么时候必须转译?

你会在两种主要情况下需要转译:

  1. 当标识符是“保留关键字”时

    • 这是我们遇到的最主要的问题。check 是用于 CHECK 约束的关键字,user 是一个内置函数/关键字。

    • 例子: SELECT check, user FROM buy; -> (失败)

    • 正确: SELECT \check`, `user` FROM buy;` -> (成功)

  2. 当标识符不规范时

    • 虽然极不推荐,但数据库允许你创建包含特殊字符、空格或小写字母的标识符(如果数据库本身大小写敏感)。

    • 例子: 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 columnSQLSyntaxErrorException 两个错误,并在修复代码后陷入了“重启-清理缓存-依然报错”的诡异循环。

以下记录了从错误发生、初步排查、错误升级、到最终通过 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.javaqueryWrapper.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") 注解得以生效。

结语:

这次排错之旅虽然曲折,但也带来了深刻的教训:

  1. 数据库关键字转译: 永远不要低估 checkuserorderkey 这些常用词。在 BuyDO.java 中使用 @TableField("列名") 加反引号,是成本最低的保险。

  2. 缓存是万恶之源: 当你的代码看起来明明正确但运行就是报错时,十有八九是缓存问题。mvn clean installBuild -> Rebuild Project 是常规操作,但有时也不可靠。

  3. 拥抱 LambdaQueryWrapper 永远优先使用 LambdaQueryWrapper。它不仅能避免“魔术字符串”带来的拼写错误,还能在编译期锁定类型,更重要的是,在这次经历中,它成了我绕过 IDE 缓存、解决灵异问题的“银弹”。


希望这篇博客能帮助到遇到类似问题的你。

更多推荐