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