返回

解决 Spring Data JPA 原生 SQL 中 LIKE 查询失效问题

mysql

解决 Spring Data JPA 中原生 SQL LIKE 查询失效问题

使用 Spring Data JPA 执行原生 SQL 查询时,LIKE 子句有时无法按预期工作。 这个问题源于不同数据库系统对于 LIKE 参数的处理方式各异,及 JDBC 预处理语句的机制。 此文会从多个方面解析 LIKE 语句在原生 SQL 中执行出错的缘由,以及相应解决方案。

LIKE 子句与参数绑定

开发过程中,在执行数据库操作,我们常利用 LIKE 子句配合通配符 %_ 进行模糊匹配。 通过使用参数绑定而非直接拼接字符串,防止 SQL 注入漏洞、提高查询效率。 不同的数据库系统 (如 PostgreSQL、MySQL) 处理 LIKE 子句的方式有所不同,而且预处理语句对参数绑定的实现机制也存在差异。 使用参数绑定的优势主要有几点:安全保障,通过参数化方式有效阻挡恶意代码;效率提升,数据库可重复利用编译过的执行计划,优化后续操作效率。

PostgreSQL 中的参数问题

在 PostgreSQL 中使用参数化的 LIKE 查询需要特殊处理。 使用 PHP 操作数据库执行 SQL 时,如果使用 % 包裹参数,如 '%$1%',则会引发问题。 因为 PostgreSQL 的 pg_query_params 函数会将 $1 当作预处理语句的占位符。但 % 不参与预处理占位符参数的构成,会导致实际绑定参数和 SQL 中要求的参数数量不符,进而导致报错。

例如,原始 SQL 查询的意图如下:

SELECT question_id, body
FROM questions
WHERE body ILIKE '%lorem%';

而实际执行却等价于以下这种错误格式:

SELECT question_id, body
FROM questions
WHERE body ILIKE '%%';

导致期望模糊匹配的结果与事实出现偏差,从而引来报错信息:bind message supplies 1 parameters, but prepared statement "" requires 0

解决方案

多种方法可以有效处理 LIKE 子句与参数绑定的组合问题。 可以参考下述方案解决实际情况。

1. 使用 CONCAT 函数 (或 || 操作符)

这种办法是把通配符和参数在 SQL 查询中拼接起来。 通过数据库内置的字符串拼接函数或者连接操作符来完成通配符与参数的绑定。

原理:

将通配符 % 作为字符串的一部分,与参数通过 CONCAT 函数或者 || (PostgreSQL 的连接符) 组合。

操作步骤:

  1. 在 SQL 查询中使用 CONCAT('%', ?, '%') 或者 '%' || ? || '%' 的形式。
  2. 在 JPA 的 @Query 注解中,确保使用 nativeQuery = true

代码示例 (Java):

public interface QuestionRepository extends JpaRepository<Question, Long> {

    @Query(value = "SELECT q FROM Question q WHERE q.body LIKE CONCAT('%', :searchTerm, '%')", nativeQuery = true)
    List<Question> findByBodyLike(@Param("searchTerm") String searchTerm);

    @Query(value = "SELECT * FROM questions WHERE body ILIKE '%' || :searchTerm || '%'", nativeQuery = true)
    List<Question> findByBodyLikePostgres(@Param("searchTerm") String searchTerm);
}

2. 使用 JPQL 的 LIKE 表达式

当查询不需要依赖于特定数据库功能时,可以放弃使用原生 SQL 而选用 JPQL。JPQL 也能很好的处理 LIKE 语句,这种情况下无需担心预处理语句导致的问题。
直接使用标准 JPQL 执行。

原理:

JPQL 作为一种面向对象的查询语言,其内部会处理 LIKE 表达式的参数。

操作步骤:

  1. 使用 LIKE 以及 %
  2. 无需在 @Query 注解中指定 nativeQuery = true

代码示例 (Java):

public interface QuestionRepository extends JpaRepository<Question, Long> {

    @Query("SELECT q FROM Question q WHERE q.body LIKE %:searchTerm%")
    List<Question> findByBodyLike(@Param("searchTerm") String searchTerm);
}

3. 程序代码中拼接字符串 (不推荐)

可以完全把需要执行的 SQL 字符串先组装好再执行。在 Java 代码中完成字符串拼接。

原理:

在将查询字符串传递给 JPA 之前,在 Java 代码中将其构建完整。

风险:

这种方法存在 SQL 注入的风险,因此强烈建议对所有用户输入数据进行转义和验证。同时不符合使用预处理语句的最佳实践要求。

操作步骤:

  1. 在 Java 代码中拼接字符串。
  2. 使用拼接后的字符串执行查询。

代码示例 (Java):

String searchTerm = "%" + userInput + "%"; // userInput 需确保安全
Query query = entityManager.createNativeQuery("SELECT * FROM questions WHERE body LIKE ?");
query.setParameter(1, searchTerm);
List<Question> results = query.getResultList();

PHP 的参数绑定修正

如果一定要在 PHP 环境使用 pg_query_params 执行 LIKE 查询。 核心修改要点:去除单引号内包裹,使用 || 运算符对变量参数做连接操作。
需要对原有的代码进行修正。核心思路是在 SQL 语句中使用 || 运算符拼接。

修正后的 PHP 代码:

$result = pg_query_params(
    $dbconn,
    "SELECT question_id, body
    FROM questions
    WHERE body ILIKE '%' || $1 || '%'",
    array($_GET['search'])
);

对数据库的操作均需要慎重考量,注意防御措施与业务执行的平衡考量。

安全性建议

安全高于一切!进行模糊搜索的系统里,确保安全性非常重要。无论使用哪种方法,都要确保对用户输入进行了适当的清理和验证。 防止可能的 SQL 注入攻击。 对任何一个数据源的外部输入参数均进行转义和合法性验证操作,在具体实践时必须对此特别谨慎。建议开发中建立白名单,严格规范数据类型和格式范围,最大可能保证安全性。

处理 LIKE 参数绑定时,需考虑查询场景的兼容性与可靠性问题。根据数据库类型,和应用环境做适应调整方案,必要的时候做好相应的错误日志监控报警,尽早发现潜在风险。