如何扩展Spring Data JPA以支持PostgreSQL自定义运算符?
当然有办法实现!不用原生查询或Specifications,你可以通过扩展Spring Data JPA的方法名解析逻辑,或者结合自定义JPA函数来支持PostgreSQL的特殊运算符,下面是两种实操方案:
方案一:扩展方法名关键字解析(完全基于方法名)
这种方式允许你像使用Spring Data默认的Contains、GreaterThan等关键字一样,自定义对应PostgreSQL运算符的方法名规则。
步骤1:定义自定义查询关键字
先创建一个枚举类,声明你的自定义关键字,对应timerange @> timestamp这个场景,我们可以定义CONTAINS_TIMESTAMP:
enum class CustomQueryKeyword : QueryKeyword { CONTAINS_TIMESTAMP; override fun getKeyword(): String { // 把下划线去掉,对应方法名里的驼峰形式 return name.replace("_", "") } }
步骤2:扩展PartTree解析逻辑
Spring Data JPA用PartTree来解析方法名,我们需要重写它的逻辑,把自定义关键字映射到PostgreSQL的@>运算符:
class CustomPartTree(source: String, domainClass: Class<*>) : PartTree(source, domainClass) { override fun createPredicate(root: Root<*>, criteriaQuery: CriteriaQuery<*>, criteriaBuilder: CriteriaBuilder): Predicate? { // 遍历方法名解析出的查询片段,替换自定义关键字对应的逻辑 val parts = this.parts return parts.firstOrNull { it.type == Part.Type.CUSTOM && it.property == "RangeContainsTimestamp" }?.let { // 生成timerange @> timestamp的Predicate val rangePath = root.get<PGTimeRange>("range") val timestampParam = it.parameters[0].value as LocalDateTime criteriaBuilder.function("range_contains", Boolean::class.java, rangePath, criteriaBuilder.literal(timestampParam)) } ?: super.createPredicate(root, criteriaQuery, criteriaBuilder) } }
步骤3:注册自定义查询处理器
创建一个配置类,替换Spring Data默认的查询处理策略,让它使用我们的自定义PartTree:
@Configuration class CustomJpaConfig : JpaRepositoryConfigExtension() { override fun postProcessQueryLookupStrategy( strategy: QueryLookupStrategy, repositoryMetadata: RepositoryMetadata, queryMethod: QueryMethod, entityManager: EntityManager, evaluationContextProvider: EvaluationContextProvider ): QueryLookupStrategy { return if (queryMethod.isQueryAnnotationPresent) { // 保留@Query注解的原有逻辑 strategy } else { // 方法名查询使用我们的自定义处理器 QueryLookupStrategy { method, em, factory, provider -> object : PartTreeJpaQuery(method, em, factory, provider) { override fun createPartTree(): PartTree { return CustomPartTree(method.name, method.returnType) } } } } } }
步骤4:编写Repository方法
现在你就可以像用默认关键字一样写方法了:
interface TimerangeRepository : JpaRepository<TimerangeEntity, Long> { // 方法名里的RangeContainsTimestamp会被解析成range @> timestamp的条件 fun findByRangeContainsTimestamp(range: PGTimeRange, timestamp: LocalDateTime): List<TimerangeEntity> }
方案二:自定义JPA函数(更轻量)
PostgreSQL的运算符本质是底层函数,我们可以把@>运算符包装成JPA可识别的自定义函数,然后在方法中调用:
步骤1:确认运算符对应的底层函数
在psql中执行\doS @>,可以查到@>对应的函数名(比如针对timerange和timestamp的可能是range_contains(timerange, timestamp))。
步骤2:在JPA中注册自定义函数
可以通过orm.xml来声明:
<!-- 在src/main/resources/META-INF/orm.xml中添加 --> <named-function name="rangeContains" class="com.yourpackage.TimerangeEntity"> <parameter name="range" type="org.postgresql.util.PGTimeRange"/> <parameter name="timestamp" type="java.time.LocalDateTime"/> <function-body><![CDATA[?1 @> ?2]]></function-body> </named-function>
步骤3:编写Repository方法
用@Query结合SpEL调用自定义函数,这种方式不算硬编码原生SQL,只是引用自定义函数:
interface TimerangeRepository : JpaRepository<TimerangeEntity, Long> { @Query("SELECT t FROM TimerangeEntity t WHERE FUNCTION('rangeContains', t.range, :timestamp) = true") fun findByRangeContainsTimestamp(@Param("timestamp") timestamp: LocalDateTime): List<TimerangeEntity> }
两种方案里,方案一更贴近你想要的“完全基于方法名”的体验,方案二实现起来更简单,不需要修改Spring Data的底层解析逻辑。
内容的提问来源于stack exchange,提问作者Pasukaru

