如何在Spring Data JPQL LIKE查询中转义'_'和'%'通配符?
问题背景
我在Spring Data Repository中定义了如下方法:
@Query(""" select t from ToneEntity t where (:title is null or (t.title like %:title%)) and (:albumId is null or (t.album.id = :albumId)) and (:artistId is null or (t.artist.id = :artistId)) and (:creatorId is null or (t.creator.id = :creatorId)) """) fun findFilteredTones( @Param("title") title: String?, @Param("albumId") albumId: Long?, @Param("artistId") artistId: Long?, @Param("creatorId") creatorId: Long?, pageable: Pageable ): List<ToneEntity>
当标题包含_或%字符时,Spring Data会将其作为SQL通配符而非字面量处理。例如数据库中存在标题为bug_with_underscore的ToneEntity,用户在Web UI输入_想要查找包含该字面量的音频,实际却返回了所有音频数据。
使用的Spring Data JPA版本为2.3.5,目前已找到两种可行方案:
- 使用SQL的concat和replace函数处理参数
- 在传入查询前手动转义通配符字符
想知道2022年是否有更优的解决方案?目前未找到比使用切面手动转义通配符更好的方法,而这类问题在项目中频繁出现。
针对Spring Data JPA 2.3.5的可行方案
1. JPQL层面直接处理(无额外代码侵入)
修改@Query中的LIKE条件,通过SQL函数对参数进行转义并指定转义字符,确保数据库将特殊字符视为字面量:
@Query(""" select t from ToneEntity t where (:title is null or (t.title like concat('%', replace(replace(:title, '_', '\\_'), '%', '\\%'), '%') escape '\\')) and (:albumId is null or (t.album.id = :albumId)) and (:artistId is null or (t.artist.id = :artistId)) and (:creatorId is null or (t.creator.id = :creatorId)) """) fun findFilteredTones( @Param("title") title: String?, @Param("albumId") albumId: Long?, @Param("artistId") artistId: Long?, @Param("creatorId") creatorId: Long?, pageable: Pageable ): List<ToneEntity>
逻辑说明:先用replace把参数中的_和%转义为\_和\%,再用concat拼接前后的%,最后通过escape '\\'告知数据库转义字符规则。
2. 自定义参数转换器(全局复用)
创建通用的字符串转义转换器,实现Converter<String, String>接口,自动处理通配符:
import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; @Component public class LikeWildcardEscapeConverter implements Converter<String, String> { @Override public String convert(String source) { if (source == null) { return null; } // 转义%、_和本身的转义符\,适配多数数据库规则 return source.replace("\\", "\\\\") .replace("%", "\\%") .replace("_", "\\_"); } }
然后在Repository方法的参数上通过@Convert指定转换器:
@Query(""" select t from ToneEntity t where (:title is null or (t.title like concat('%', :title, '%') escape '\\')) and (:albumId is null or (t.album.id = :albumId)) and (:artistId is null or (t.artist.id = :artistId)) and (:creatorId is null or (t.creator.id = :creatorId)) """) fun findFilteredTones( @Param("title") @Convert(converter = LikeWildcardEscapeConverter::class) title: String?, @Param("albumId") albumId: Long?, @Param("artistId") artistId: Long?, @Param("creatorId") creatorId: Long?, pageable: Pageable ): List<ToneEntity>
这种方案可全局复用转换器,避免每个查询重复编写转义逻辑。
3. 切面统一处理(批量解决同类问题)
如果项目中有大量类似的LIKE查询,使用切面可实现零侵入的全局统一转义:
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LikeParameterEscapeAspect { @Before("execution(* com.yourpackage.repository.*.*(.., String+, ..))") public void escapeLikeParameters(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; if (arg instanceof String) { String str = (String) arg; // 转义通配符,可结合自定义注解(如@EscapeLike)精准匹配需要转义的参数 String escaped = str.replace("\\", "\\\\") .replace("%", "\\%") .replace("_", "\\_"); args[i] = escaped; } } } }
注意:需要根据项目实际的Repository包路径调整切点表达式,也可自定义注解标记需要转义的参数,避免误处理非LIKE查询的字符串参数。
关于"更优方案"的说明
在Spring Data JPA 2.3.x版本(包括2022年的版本),框架本身并未提供内置的自动转义LIKE通配符的功能。上述三种方案中:
- 自定义参数转换器兼顾复用性和代码侵入性,是推荐的常规方案;
- 切面处理适合批量解决项目中大量同类查询的转义需求,但需注意参数的精准匹配,避免影响其他业务逻辑。
内容的提问来源于stack exchange,提问作者Jean Ponomarev

