SQL LIKE 模糊搜索特殊字符转义实战指南

SQL LIKE 模糊搜索特殊字符转义实战指南

一、问题描述

在使用 SQL LIKE 进行模糊搜索时,%_\ 是特殊字符:

  • % --- 匹配任意数量的任意字符
  • _ --- 匹配单个任意字符
  • \ --- 转义字符

如果用户在搜索框中输入了这些字符(如搜索 100%user_name),不做处理会导致搜索逻辑异常。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、问题演示

2.1 正常搜索

sql 复制代码
-- 搜索包含"张三"的记录
SELECT * FROM employee WHERE name LIKE '%张三%';
-- 正确匹配:张三、张三丰、大张三

2.2 异常场景

sql 复制代码
-- 用户输入 "100%" 搜索,期望搜索包含"100%"字面量的记录
-- 实际 SQL:
SELECT * FROM employee WHERE name LIKE '%100%%';
-- 等价于 LIKE '%100%'(最后的 % 被当作通配符),匹配所有包含"100"的记录
-- 用户输入 "___" 搜索
-- 实际 SQL:
SELECT * FROM employee WHERE name LIKE '%___%';
-- _ 是通配符,匹配任意3个字符,等价于搜索长度≥3的所有记录
-- 用户输入 "%%%" 搜索
-- 实际 SQL:
SELECT * FROM employee WHERE name LIKE '%%%%%';
-- 等价于 LIKE '%',匹配所有记录,搜索条件完全失效

三、解决方案

3.1 方案一:Java 层转义(推荐)

在传入 SQL 之前,对用户输入进行预处理:

java 复制代码
/**
 * 转义LIKE查询中的特殊字符.
 *
 * @param param 用户原始输入
 * @return 转义后的安全字符串
 */
private String escapeLikeParam(String param) {
    if (param == null) {
        return null;
    }
    return param
        .replace("\\", "\\\\")  // 先转义反斜杠本身
        .replace("%", "\\%")    // 再转义 %
        .replace("_", "\\_");   // 再转义 _
}

使用方式:

java 复制代码
@Override
public PageInfo<EmployeeDto> listEmployee(QueryParamsDto paramsDto) {
    // 在查询前转义模糊搜索参数
    if (StringUtils.isNotBlank(paramsDto.getName())) {
        paramsDto.setName(escapeLikeParam(paramsDto.getName()));
    }
    if (StringUtils.isNotBlank(paramsDto.getCode())) {
        paramsDto.setCode(escapeLikeParam(paramsDto.getCode()));
    }
    return employeeMapper.listEmployee(paramsDto);
}

SQL 保持不变:

xml 复制代码
<if test="param.name != null and param.name != ''">
    AND e.name LIKE CONCAT('%', #{param.name}, '%')
</if>

3.2 方案二:SQL 层转义

直接在 MyBatis XML 中使用 REPLACE 函数:

xml 复制代码
<if test="param.name != null and param.name != ''">
    AND e.name LIKE CONCAT('%',
        REPLACE(REPLACE(REPLACE(#{param.name}, '\\', '\\\\'), '%', '\\%'), '_', '\\_'),
        '%')
</if>

3.3 方案三:SQL ESCAPE 子句

MySQL 支持 ESCAPE 关键字指定自定义转义字符:

xml 复制代码
<if test="param.name != null and param.name != ''">
    AND e.name LIKE CONCAT('%', #{param.name}, '%') ESCAPE '/'
</if>

Java 中用 / 作为转义符:

java 复制代码
private String escapeLikeParam(String param) {
    if (param == null) return null;
    return param
        .replace("/", "//")
        .replace("%", "/%")
        .replace("_", "/_");
}

3.4 方案对比

方案 优点 缺点 适用场景
Java 层转义 简单直观,不改 SQL 需要每个搜索字段都处理 通用推荐
SQL REPLACE 不改 Java 代码 SQL 可读性差 不方便改 Java 时
ESCAPE 子句 标准 SQL 语法 需要统一转义字符 严格规范的项目

四、完整示例

4.1 Service 层

java 复制代码
@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Resource
    private EmployeeMapper employeeMapper;

    @Override
    public PageInfo<EmployeeDto> listEmployee(EmployeeQueryDto queryDto) {
        // 模糊搜索参数转义
        if (StringUtils.isNotBlank(queryDto.getName())) {
            queryDto.setName(escapeLikeParam(queryDto.getName()));
        }
        if (StringUtils.isNotBlank(queryDto.getPhone())) {
            queryDto.setPhone(escapeLikeParam(queryDto.getPhone()));
        }
        if (StringUtils.isNotBlank(queryDto.getEmail())) {
            queryDto.setEmail(escapeLikeParam(queryDto.getEmail()));
        }

        List<EmployeeDto> list = employeeMapper.listEmployee(queryDto);
        return new PageInfo<>(list);
    }

    /**
     * 转义LIKE查询中的特殊字符.
     */
    private String escapeLikeParam(String param) {
        if (param == null) {
            return null;
        }
        return param
            .replace("\\", "\\\\")
            .replace("%", "\\%")
            .replace("_", "\\_");
    }
}

4.2 Mapper XML

xml 复制代码
<select id="listEmployee" resultType="com.example.dto.EmployeeDto">
    SELECT id, name, phone, email, dept_code
    FROM employee
    <where>
        <if test="param.name != null and param.name != ''">
            AND name LIKE CONCAT('%', #{param.name}, '%')
        </if>
        <if test="param.phone != null and param.phone != ''">
            AND phone LIKE CONCAT('%', #{param.phone}, '%')
        </if>
        <if test="param.email != null and param.email != ''">
            AND email LIKE CONCAT('%', #{param.email}, '%')
        </if>
    </where>
    ORDER BY id DESC
</select>

4.3 转义效果对照

用户输入 转义后 实际 SQL LIKE 匹配行为
张三 张三 '%张三%' 正常模糊搜索
100% 100\% '%100\%%' 搜索包含"100%"字面量
user_name user\_name '%user\_name%' 搜索包含"user_name"字面量
%%% \%\%\% '%\%\%\%%' 搜索包含"%%%"字面量
a\b a\\b '%a\\b%' 搜索包含"a\b"字面量

五、注意事项

5.1 转义顺序很重要

java 复制代码
// 正确顺序:先转义反斜杠,再转义 % 和 _
param.replace("\\", "\\\\")  // 第一步
    .replace("%", "\\%")      // 第二步
    .replace("_", "\\_");     // 第三步

// 错误顺序:如果先转义 %,后转义 \
// "100%" → "100\%" → "100\\%" (反斜杠被二次转义,结果错误)

5.2 #{} vs ${}

xml 复制代码
<!-- 安全:#{} 使用预编译,防止 SQL 注入 -->
AND name LIKE CONCAT('%', #{param.name}, '%')

<!-- 危险:${} 直接拼接,存在 SQL 注入风险 -->
AND name LIKE '%${param.name}%'

#{} 已经防止了 SQL 注入,但不防止 LIKE 通配符问题。两者是不同层面的安全问题:

  • SQL 注入:通过 #{} 解决(预编译参数化)
  • LIKE 通配符:通过转义解决(本文内容)

5.3 前端 vs 后端处理

处理位置 优点 缺点
前端转义 减少后端逻辑 不可靠(可绕过)
后端转义 安全可靠 需要每个字段处理

建议:始终在后端转义,前端可以做提示但不能依赖。

5.4 空字符串判断

java 复制代码
// 转义前先判空,空字符串不需要转义
if (StringUtils.isNotBlank(queryDto.getName())) {
    queryDto.setName(escapeLikeParam(queryDto.getName()));
}

如果不判空,空字符串转义后还是空字符串,虽然不会报错,但多一次无意义的处理。


六、工具类封装

如果项目中多处需要模糊搜索转义,建议封装为工具类:

java 复制代码
public final class SqlLikeUtil {

    private SqlLikeUtil() {
    }

    /**
     * 转义 SQL LIKE 查询中的特殊字符(%、_、\).
     *
     * @param param 原始输入
     * @return 转义后的字符串,null 输入返回 null
     */
    public static String escape(String param) {
        if (param == null) {
            return null;
        }
        return param
            .replace("\\", "\\\\")
            .replace("%", "\\%")
            .replace("_", "\\_");
    }

    /**
     * 判断字符串是否包含 LIKE 特殊字符.
     */
    public static boolean containsSpecialChar(String param) {
        if (param == null) {
            return false;
        }
        return param.contains("%") || param.contains("_") || param.contains("\\");
    }
}

使用:

java 复制代码
if (StringUtils.isNotBlank(queryDto.getName())) {
    queryDto.setName(SqlLikeUtil.escape(queryDto.getName()));
}

七、最佳实践清单

  1. 所有模糊搜索字段都要转义:不要遗漏任何一个 LIKE 查询的入参
  2. 转义在后端 Service 层做:不依赖前端处理
  3. 转义顺序:先 \ 再 % 再 _:避免二次转义
  4. 使用 CONCAT + #{}:既防注入又支持转义
  5. 不要用 ${} 拼接 LIKE:存在 SQL 注入风险
  6. 空值判断在转义前:避免 NPE
  7. 封装工具类复用:多个模块统一使用
  8. 单元测试覆盖特殊字符 :测试 %_\%%、空字符串等场景