Spring数据-如何将YearMonth类型的@Param转换为LocalDate的开始和结束?

Spring Data - How to Convert @Param of type YearMonth to start and end LocalDate?

背景域

我有一个产品的费率,每月存储在数据库中。开始日期始终是上个月的最后一天,结束日期是当月的最后一天。例如,2020 年 3 月汇率的开始日期为 2020-02-29,结束日期为 2020-03-31。

当前实施

这是 Rate class:

@Entity
@Data
public class Rate {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name="productId")
    @JsonBackReference
    private Product product;
    
    private LocalDate startDate;
    private LocalDate endDate;
    private BigDecimal value;
    
}

我有一个 Spring 数据存储库,它具有以下方法:

Rate findByProductIdAndStartDateAndEndDate(
        @Param("productId") Long productId,
        @Param("startDate") @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate startDate, 
        @Param("endDate") @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate endDate);

目标

我想通过(例如)通过重写 findByProductIdAndStartDateAndEndDate 调用 Rate 存储库来从客户那里抽象出这个细节:

//OK with writing JPQL here via @Query
Rate findByProductIdAndYearMonth(
        @Param("productId") Long productId,
        @Param("yearMonth") @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) YearMonth yearMonth);

我尝试过/考虑过的

我在阅读 this post 后考虑过 AttributeConverter。但我正在尝试从一个字段 yearMonth 转换为两个数据库列 startDateendDate.

我知道我可以编写一个服务 class 来获取 YearMonth,然后在确定开始日期和结束日期后调用存储库。但我正在使用 Spring Data REST 并希望允许客户通过以下方式直接调用此 repo:

/api/rates/search/findByProductIdAndYearMonth

您始终可以做的是使用 SPeL 调用 @Query 中的自定义方法来为您进行转换。例如创建一个实用程序 class like

package com.acme.util;

import java.time.LocalDate;
import java.time.YearMonth;

public class RateDateUtil {

   public static LocalDate toEndDate(YearMonth ym) {
     return ym.atEndOfMonth();
   }
    
   public static LocalDate toStartDate(YearMonth ym) {
     return ym.minusMonths(1).atEndOfMonth();
   }
}

然后在你的@Query

中使用它
@Query("from Rate r join r.product p where p.id = :#{#productId} and r.startDate = :#{T(com.acme.util.RateDateUtil).toStartDate(#yearMonth)} and r.endDate = :#{T(com.acme.util.RateDateUtil).toEndDate(#yearMonth)}")
Rate findByProductIdAndYearMonth(@Param("productId") Long productId, @Param("yearMonth") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) YearMonth yearMonth);

请注意,在较旧的 spring-data-jpa 版本中,您不能混合使用 SPeL 和 JPA 方式,这就是 productId 也需要包装的原因。这可能已在较新版本中得到修复(我使用 1.1.x 版本对此进行了测试)。有关详细信息,请参阅 this answer 另一个问题。

如果您的数据库支持虚拟列,那么我会考虑创建一个名为 period 的虚拟列,计算为结束日期的 month/year。

如果该虚拟列与任何其他虚拟列一样映射到您的实体中 属性,那么查询就变得微不足道了。您可以将 JPA 属性 设为只读。一般来说,这里的查询可能有优势,因为任何查询都可以简单地写为句点 = '0320' 或其他任何内容,而不必担心开始和结束日期。

如果您的数据库不支持虚拟列,那么您可以通过 2 列数据库视图(rate_id,句点)实现类似的效果,然后通过 @SecondaryTable 连接到 Rate 实体.

在任一解决方案中,您的 Rate 实体都有一个新的 属性 period,您可以像对任何其他列一样查询和排序。