【postgresql】JPA LIKE 查询触发 PostgreSQL `text ~~ bytea` 报错的排查与最佳实践

JPA LIKE 查询触发 PostgreSQL text ~~ bytea 报错的排查与最佳实践

  • 日期:2025-09-04
  • 项目:procurement-platform

背景与环境

  • 技术栈:Spring Boot + Spring Data JPA + Hibernate + PostgreSQL 16
  • 配置:spring.jpa.hibernate.ddl-auto=update
  • 依赖:Postgres、Redis 均通过 docker-compose 启动,应用连接本机端口

现象

分页查询接口在包含关键字 keyword 条件时抛错:

ERROR: operator does not exist: text ~~ bytea
建议:No operator matches the given name and argument types. You might need to add explicit type casts.

Hibernate 打印的 SQL(简化):

... and (? is null or lower(re1_0.title) like ('%'||?||'%') escape '') ...

报错含义:LIKE 操作符两侧类型不匹配,左侧为 text(列 title),右侧被绑定成了 bytea(? 参数)。

初步排查

  • 数据库表结构确认(容器内):
    • requirements.titlevarchar(200),无异常。
    • 存在函数索引:idx_requirements_title_lower (lower(title::text)),利于性能。
  • 代码类型确认:
    • RequirementEntity.title: String?
    • JPA 方法 findWithFilters(@Param("keyword") keyword: String?),类型为 String?
    • Controller/Service/RepositoryImpl 传参链路均为 String?

结论:表结构与实体定义均正确;问题出在 JDBC 参数绑定阶段,keyword 被当成了 bytea 绑定。

根因分析(为何会变成 bytea)

  • 当使用 LOWER(r.title) LIKE CONCAT('%', :keyword, '%') 这类表达式时,Hibernate/驱动在特定场景可能无法准确推断参数的字符类型,或受上游入参处理影响(例如非文本内容、全局转换器等),导致把 :keyword 绑定为 VARBINARY/bytea。
  • 本项目已启用 org.hibernate.type.descriptor.sql.BasicBinder: TRACE,可通过日志直接确认参数的绑定类型(建议在出现问题时查看)。

解决方案

采用“参数化通配模式 + 代码侧预处理”的方式,确保以 VARCHAR 绑定:

  1. JPQL 仅对列使用 LOWER,LIKE 使用参数自身作为完整模式:

文件:RequirementJpaRepository.kt

@Query("""
    SELECT r FROM RequirementEntity r 
    WHERE (:buyerId IS NULL OR r.buyerId = :buyerId)
    AND (:status IS NULL OR r.status = :status)
    AND (:categoryId IS NULL OR r.categoryId = :categoryId)
    AND (:dataType IS NULL OR r.dataType = :dataType)
    AND (:keyword IS NULL OR LOWER(r.title) LIKE :keyword)
    AND (:includeDeleted = true OR r.deleted = false)
    ORDER BY r.createdAt DESC
""")
fun findWithFilters(
    @Param("buyerId") buyerId: UUID?,
    @Param("status") status: String?,
    @Param("categoryId") categoryId: Long?,
    @Param("keyword") keyword: String?,
    @Param("dataType") dataType: String?,
    @Param("includeDeleted") includeDeleted: Boolean,
    pageable: Pageable
): Page<RequirementEntity>
  1. 实现层预处理 keyword:去空格、判空、小写,并拼接通配符,传入完整模式字符串 %xxx%

文件:RequirementRepositoryImpl.kt

val normalizedKeyword = keyword
    ?.trim()
    ?.takeIf { it.isNotEmpty() }
    ?.lowercase()
val likePattern = normalizedKeyword?.let { "%$it%" }

val entityPage = jpaRepository.findWithFilters(
    buyerId = buyerId?.value,
    status = status?.code,
    categoryId = categoryId?.value,
    keyword = likePattern,
    dataType = dataType?.code,
    includeDeleted = includeDeleted,
    pageable = pageable
)

效果:

  • 通配符由应用侧拼好,JPA 直接将 :keyword 当作字符串绑定;
  • 避免 CONCAT 与参数组合带来的类型推断问题;
  • 维持“JPQL 仅对列 LOWER、参数在实现层 lowercase”的最佳实践。

验证

  • 构建通过:./gradlew build -x test
  • 接口测试:GET /api/v1/requirements?keyword=... 正常,无 text ~~ bytea 报错;
  • 大小写不敏感:LOWER(r.title) + 代码侧 lowercase() 保证一致性;
  • 日志可见参数以 VARCHAR 绑定(如需进一步确认,可查看 BasicBinder TRACE)。

关于 ddl-auto=update 与容器删除后的错误

  • ddl-auto=update 只在“连接成功”后才会对已存在的数据库执行建表/更新,不负责启动数据库或开放端口。
  • 删除容器后需要先重新启动依赖:
    • docker compose up -d postgres redis
    • 待 Postgres 健康检查通过后再 ./gradlew bootRun

性能与长期治理建议

  • 已有 LOWER(title) 函数索引可提升 LIKE 查询性能;若未创建,建议:
    CREATE INDEX IF NOT EXISTS idx_requirements_title_lower ON requirements (LOWER(title));
    
  • 开发阶段可继续使用 ddl-auto=update
  • 长期建议引入 Flyway/Liquibase 管理 DDL 与索引,提升可追溯性与可维护性。

Checklist

  • 修复 LIKE 绑定为 bytea 的问题
  • 保持 JPQL 可移植性与可读性
  • 保持 ddl:auto=update 兼容
  • 性能可用(函数索引)

小结

问题根因在于 JDBC 参数绑定类型被误判为 bytea。通过“JPQL 使用 LIKE :keyword + 实现层构造 %xxx% 的字符串参数”的方式,强制以字符串绑定,从而彻底规避 text ~~ bytea 报错。同时保持了大小写不敏感匹配的一致性与可维护性。

在使用 PostgreSQL 数据库时,可以使用 PostgreSQL 内置的加密函数或自定义函数来实现解密操作。以下是一种可能的解决方案: 1. 创建一个加密转换器类,实现 `javax.persistence.AttributeConverter` 接口,重写 `convertToDatabaseColumn` 和 `convertToEntityAttribute` 方法,用于加密和解密数据。例如: ```java import javax.persistence.AttributeConverter; import javax.persistence.Converter; @Converter public class EncryptionConverter implements AttributeConverter<String, String> { @Override public String convertToDatabaseColumn(String attribute) { // 实现加密逻辑 return encryptedValue; } @Override public String convertToEntityAttribute(String dbData) { // 实现解密逻辑 return decryptedValue; } } ``` 2. 在需要加密的属性上添加 `@Convert` 注解,指定使用的转换器。例如: ```java @Entity public class User { @Id private Long id; @Convert(converter = EncryptionConverter.class) private String password; // 其他属性和方法... } ``` 3. 在 SQL 查询中使用 PostgreSQL 内置的解密函数或自定义函数进行解密。以下是一个示例: ```sql SELECT id, pgp_sym_decrypt(password::bytea, 'encryptionKey') AS decrypted_password FROM user; ``` 这里使用 PostgreSQL 的 `pgp_sym_decrypt` 函数对密码进行解密,`encryptionKey` 是加密时使用的密钥。 需要注意的是,直接在 SQL 查询中解密数据可能会影响性能,并且可能会暴露加密密钥。因此,在实际应用中,建议在应用层面进行解密操作,而不是直接在 SQL 查询中解密。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值