🌟 场景描述

在许多项目中,为了简化表结构,我们会将一对多的标签数据序列化为 JSON 字符串,存入数据库的 TEXT 字段。例如:

-- 表结构示例
CREATE TABLE question (
    id BIGINT PRIMARY KEY,
    title VARCHAR(255),
    tags TEXT  -- 存储如:["Java", "Spring", "MyBatis"]
);

前端通过接口传入一个标签列表:

{
  "tags": ["Java", "Redis"]
}

需求:查询所有同时包含 “Java” 和 “Redis” 标签的问题(即交集查询)。

💡 注意:虽然字段是 TEXT,但内容是合法的 JSON 数组格式,因此仍可借助数据库的 JSON 函数进行解析。

⚠️ 为什么不推荐 LIKE 模糊匹配?

一般我们会这样写:

wrapper.like("tags", "%Java%").like("tags", "%Redis%");

但这种方式存在严重缺陷:

  • ❌ 误匹配:"JavaScript" 会被 "Java" 错误命中;

  • ❌ 边界问题:无法区分 "Go" 和 "Golang";

  • ❌ 语义错误:破坏了 JSON 数据的结构化含义。

✅ 正确方案:使用 JSON_CONTAINS 解析 TEXT 中的 JSON

即使 tagsTEXT 类型,只要其内容是合法的 JSON 字符串,MySQL 5.7+ 依然能通过 JSON_CONTAINS 函数正确解析并匹配。

🔧 实现代码(MyBatis-Plus)

import cn.hutool.core.collection.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;

public LambdaQueryWrapper<Question> buildQueryWrapper(List<String> tags) {
    LambdaQueryWrapper<Question> queryWrapper = Wrappers.lambdaQuery();

    if (!CollectionUtils.isEmpty(tags)) {
        for (String tag : tags) {
            // 将 tag 转为 JSON 字符串字面量:"tag"
            String jsonTag = "\"" + tag + "\"";
            queryWrapper.apply("JSON_CONTAINS(tags, ?)", jsonTag);
        }
    }

    return queryWrapper;
}

💡 关键说明

要点

说明

JSON_CONTAINS(tags, ?)

MySQL 会自动将 tags(TEXT)按 JSON 规则解析,再判断是否包含指定值。

"\" + tag + \""

必须将 Java 字符串包装成 JSON 字符串(带双引号),否则 JSON_CONTAINS 会将其视为数字或布尔值,导致匹配失败。

多次 apply

每次调用生成 AND JSON_CONTAINS(...),实现“必须同时包含所有标签”的逻辑。

安全性

使用 ? 占位符,由 MyBatis 自动参数化,无 SQL 注入风险。

🧪 生成的 SQL 示例

当前端传入 ["Java", "Redis"] 时,实际执行的 SQL 为:

SELECT * FROM question 
WHERE JSON_CONTAINS(tags, 'Java') 
  AND JSON_CONTAINS(tags, 'Redis');

✅ 精确匹配:["JavaScript", "Redis"] 不会被查出

✅ 结构感知:正确识别 JSON 数组中的独立元素

JSON_CONTAINSTEXT 字段上无法使用普通索引