Spring 数据 JPA 检索两个日期之间的数据无效
Spring data JPA retrieve data with between two dates not working
已编辑:
我试图通过按日期过滤数据来获取数据,但出现以下错误。我接受 LocalDateTimeFormat
中的日期。但是,created_date
的类型在 JPA 实体中是 Instant
。我无法更改实体中的类型,因为它可能会破坏其他 API
Parameter value [2021-12-23T13:00] did not match expected type [java.time.Instant (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [2021-12-23T13:00] did not match expected type [java.time.Instant (n/a)]
卷曲请求
curl --location --request GET 'http://localhost:9202/movement-history?amd=xs&fromCreatedDate=2021-12-23T13:00&toCreatedDate=2021-12-23T15:10'
回应
{
"code": 0,
"message": "Success",
"data": {
"movementHistory": [],
"totalCount": 0,
"page": 1,
"limit": 20
}
}
控制器
@GetMapping( value = "/movement-history")
public ResponseDto<RetrieveMovementHistoryResponse> retrieveMovementHistory(
@Valid RetrieveMovementHistoryRequest retrieveMovementHistoryRequest
) {
return responseUtil.prepareSuccessResponse( retrieveHistoryService.doAction( retrieveMovementHistoryRequest ),
null );
}
检索移动历史请求 DTO
package ai.growing.platform.inventory.dto;
import ai.growing.platform.library.dto.Request;
import lombok.*;
import lombok.experimental.FieldDefaults;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults( level = AccessLevel.PRIVATE )
public class RetrieveMovementHistoryRequest extends Request {
List<String> amd;
@DateTimeFormat( pattern = "yyyy-MM-dd'T'HH:mm" )
LocalDateTime fromCreatedDate;
@DateTimeFormat( pattern = "yyyy-MM-dd'T'HH:mm" )
LocalDateTime toCreatedDate;
@Positive( message = "limit can not be negative" )
@Builder.Default
@NotNull
Integer limit = 20;
@Builder.Default
@Range( min = 1, message = "page can not be zero or negative" )
@NotNull
Integer page = 1;
}
JPA 的搜索规范
public class TransactionSearchSpecification {
private static final String AMD = "amd";
private static final String CREATED_DATE = "createdDate";
public static Specification<Transaction> getTransactionBySpecification(
TransactionSearchCriteria transactionSearchCriteria) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if( !CollectionUtils.isEmpty( transactionSearchCriteria.getAmdList() ) ) {
predicates.add( root.get( SKU ).in( transactionSearchCriteria.getAmdList() ) );
}
if( !StringUtils.isEmpty( transactionSearchCriteria.getFromDate() ) && !StringUtils.isEmpty(
transactionSearchCriteria.getToDate() ) ) {
predicates.add( criteriaBuilder
.between( root.get( CREATED_DATE )
, transactionSearchCriteria.getFromDate(), transactionSearchCriteria.getToDate() ) );
}
return criteriaBuilder.and( predicates.toArray( new Predicate[ 0 ] ) );
};
}
}
实体
package ai.growing.platform.product.inventory.entity;
import java.io.Serializable;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@Builder
@Entity
@Table( name = "transaction" )
@Data
@NoArgsConstructor
public class Transaction implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue( strategy = GenerationType.IDENTITY )
@Column( name = "id", nullable = false )
private Long id;
@Column( name = "amd", nullable = false )
private String amd;
@Column( name = "request_type" )
private String requestType;
@CreationTimestamp
@Column( name = "created_date", nullable = false )
private Instant createdDate;
}
数据库中的数据
id created_date request_type amd
850476 2021-12-23 13:01:19 COMPLETED xs
850480 2021-12-23 14:58:17 COMPLETED xs
850474 2021-12-23 13:00:41 INITIATED xs
850478 2021-12-23 14:58:08 INITIATED xs
JPA 发出的查询
select
transactio0_.id as id1_11_,
transactio0_.created_date as created_2_11_,
transactio0_.amd as amd3_11_,
from
transaction transactio0_
where
(
transactio0_.amd in (
?
)
)
and (
transactio0_.created_date between ? and ?
) limit ?
2022-01-24 09:02:00.918 TRACE [...true] 29314 --- [tp1324897979-85] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [xs]
2022-01-24 09:02:00.918 TRACE [...,true] 29314 --- [tp1324897979-85] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [TIMESTAMP] - [2021-12-23T13:00:00Z]
2022-01-24 09:02:00.920 TRACE [...,true] 29314 --- [tp1324897979-85] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [TIMESTAMP] - [2021-12-23T15:10:00Z]
正如您在堆栈跟踪中看到的那样,该错误与您提供的值 2021-12-23T13:00
到 Instant
之间的转换有关,可能是在 Spring 数据执行 JPA Criteria 查询时您在 Specification
.
中创建
要解决此问题,您可以尝试在 Specification
代码中手动将 LocalDateTime
转换为 Instant
- 我假设 transactionSearchCriteria.getFromDate()
和 transactionSearchCriteria.getToDate()
是 String
。例如:
public class TransactionSearchSpecification {
private static final String AMD = "amd";
private static final String CREATED_DATE = "createdDate";
public static Specification<Transaction> getTransactionBySpecification(
TransactionSearchCriteria transactionSearchCriteria) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if( !CollectionUtils.isEmpty( transactionSearchCriteria.getAmdList() ) ) {
predicates.add( root.get( SKU ).in( transactionSearchCriteria.getAmdList() ) );
}
String fromDate = transactionSearchCriteria.getFromDate();
String toDate = transactionSearchCriteria.getToDate();
if( !StringUtils.isEmpty( fromDate ) && !StringUtils.isEmpty(
toDate ) {
try {
Instant fromInstant = this.fromString(fromDate);
Instant toInstant = this.fromString(toDate);
predicates.add(
criteriaBuilder.between(
root.get( CREATED_DATE ), fromInstant, toInstant
)
);
} catch (DateTimeParseException dtpe) {
// invalid format, consider log the error, etcetera
dtpe.printStackTrace();
}
}
return criteriaBuilder.and( predicates.toArray( new Predicate[ 0 ] ) );
};
}
// This method is suitable for being defined probably in a common, utility class
// Following, for example, the advice provided in this SO question
//
private Instant fromString(final String localDateTimeString) throws DateTimeParseException {
if (StringUtils.isEmpty(localDateTimeString) {
return null;
}
// Convert String to LocalDateTime
LocalDateTime localDateTime = LocalDateTime.parse(
localDateTimeString,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm" , Locale.US )
);
// Provide the appropriate ZoneId
Instant result = localDateTime
.atZone(ZoneId.of("Asia/Kolkata"))
.toInstant();
// Return the obtained instant
result;
}
}
我不清楚 RetrieveProductMovementHistoryRequest
的作用。如果 Specification
以任何方式涉及使用 LocalDateTime fromCreatedDate
,那么我的建议是您需要手动调整 Specification
中的代码来处理 [=13] =] 值。我的意思是:
public class TransactionSearchSpecification {
private static final String AMD = "amd";
private static final String CREATED_DATE = "createdDate";
public static Specification<Transaction> getTransactionBySpecification(
TransactionSearchCriteria transactionSearchCriteria) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if( !CollectionUtils.isEmpty( transactionSearchCriteria.getAmdList() ) ) {
predicates.add( root.get( SKU ).in( transactionSearchCriteria.getAmdList() ) );
}
LocalDateTime fromDate = transactionSearchCriteria.getFromDate();
LocalDateTime toDate = transactionSearchCriteria.getToDate();
if( fromDate != null && toDate != null ) {
Instant fromInstant = this.fromLocalDateTime(fromDate);
Instant toInstant = this.fromLocalDateTime(toDate);
predicates.add(
criteriaBuilder.between(
root.get( CREATED_DATE ), fromInstant, toInstant
)
);
}
return criteriaBuilder.and( predicates.toArray( new Predicate[ 0 ] ) );
};
}
// Again, this method is suitable for being defined probably in a common, utility class
private Instant fromLocalDateTime(final LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
// Provide the appropriate ZoneId
Instant result = localDateTime
.atZone(ZoneId.of("Asia/Kolkata"))
.toInstant();
// Return the obtained instant
result;
}
}
对于您的评论,您似乎没有得到任何结果。
第一步,尝试调试和隔离错误:务必检查您的存储库是否实际返回任何结果。
如果它从数据库返回适当的记录,请检查您的服务代码,即与 Specification
交互的代码。
如果您的存储库没有返回任何结果,则问题可能与不同的事情有关。
请注意,主要是我们使用的是时间点。正如您在 MySql documentation 中看到的那样,根据不同的因素,尤其是 CREATED_DATE
列的类型,TIMESTAMP
或 DATETIME
,您可能会获得意想不到的结果执行查询时的结果,这可以解释为什么您没有获得任何结果。
我发现了问题,这可能是 JPA 如何与 MySQL 或 MySQL 本身交互的一种方式。问题是 fromCreatedDate
和 toCreatedDate
在执行时被转换为 localDateTime
。例如,我发送的日期是即时 2021-12-20T13:00:41Z
和 2021-12-29T15:10:08Z
,它被转换为阿联酋本地时间 2021-12-23 17:00:41.0
和 2021-12-23 19:10:08.0
。所以代码工作正常,但因为那天没有记录。我给了我空结果。
我启用 MySql 常规日志以查看实际查询到 MySql 的查询。我参考了 this 视频以在 MySql 中启用日志。
已编辑:
我试图通过按日期过滤数据来获取数据,但出现以下错误。我接受 LocalDateTimeFormat
中的日期。但是,created_date
的类型在 JPA 实体中是 Instant
。我无法更改实体中的类型,因为它可能会破坏其他 API
Parameter value [2021-12-23T13:00] did not match expected type [java.time.Instant (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [2021-12-23T13:00] did not match expected type [java.time.Instant (n/a)]
卷曲请求
curl --location --request GET 'http://localhost:9202/movement-history?amd=xs&fromCreatedDate=2021-12-23T13:00&toCreatedDate=2021-12-23T15:10'
回应
{
"code": 0,
"message": "Success",
"data": {
"movementHistory": [],
"totalCount": 0,
"page": 1,
"limit": 20
}
}
控制器
@GetMapping( value = "/movement-history")
public ResponseDto<RetrieveMovementHistoryResponse> retrieveMovementHistory(
@Valid RetrieveMovementHistoryRequest retrieveMovementHistoryRequest
) {
return responseUtil.prepareSuccessResponse( retrieveHistoryService.doAction( retrieveMovementHistoryRequest ),
null );
}
检索移动历史请求 DTO
package ai.growing.platform.inventory.dto;
import ai.growing.platform.library.dto.Request;
import lombok.*;
import lombok.experimental.FieldDefaults;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults( level = AccessLevel.PRIVATE )
public class RetrieveMovementHistoryRequest extends Request {
List<String> amd;
@DateTimeFormat( pattern = "yyyy-MM-dd'T'HH:mm" )
LocalDateTime fromCreatedDate;
@DateTimeFormat( pattern = "yyyy-MM-dd'T'HH:mm" )
LocalDateTime toCreatedDate;
@Positive( message = "limit can not be negative" )
@Builder.Default
@NotNull
Integer limit = 20;
@Builder.Default
@Range( min = 1, message = "page can not be zero or negative" )
@NotNull
Integer page = 1;
}
JPA 的搜索规范
public class TransactionSearchSpecification {
private static final String AMD = "amd";
private static final String CREATED_DATE = "createdDate";
public static Specification<Transaction> getTransactionBySpecification(
TransactionSearchCriteria transactionSearchCriteria) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if( !CollectionUtils.isEmpty( transactionSearchCriteria.getAmdList() ) ) {
predicates.add( root.get( SKU ).in( transactionSearchCriteria.getAmdList() ) );
}
if( !StringUtils.isEmpty( transactionSearchCriteria.getFromDate() ) && !StringUtils.isEmpty(
transactionSearchCriteria.getToDate() ) ) {
predicates.add( criteriaBuilder
.between( root.get( CREATED_DATE )
, transactionSearchCriteria.getFromDate(), transactionSearchCriteria.getToDate() ) );
}
return criteriaBuilder.and( predicates.toArray( new Predicate[ 0 ] ) );
};
}
}
实体
package ai.growing.platform.product.inventory.entity;
import java.io.Serializable;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@Builder
@Entity
@Table( name = "transaction" )
@Data
@NoArgsConstructor
public class Transaction implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue( strategy = GenerationType.IDENTITY )
@Column( name = "id", nullable = false )
private Long id;
@Column( name = "amd", nullable = false )
private String amd;
@Column( name = "request_type" )
private String requestType;
@CreationTimestamp
@Column( name = "created_date", nullable = false )
private Instant createdDate;
}
数据库中的数据
id created_date request_type amd
850476 2021-12-23 13:01:19 COMPLETED xs
850480 2021-12-23 14:58:17 COMPLETED xs
850474 2021-12-23 13:00:41 INITIATED xs
850478 2021-12-23 14:58:08 INITIATED xs
JPA 发出的查询
select
transactio0_.id as id1_11_,
transactio0_.created_date as created_2_11_,
transactio0_.amd as amd3_11_,
from
transaction transactio0_
where
(
transactio0_.amd in (
?
)
)
and (
transactio0_.created_date between ? and ?
) limit ?
2022-01-24 09:02:00.918 TRACE [...true] 29314 --- [tp1324897979-85] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [xs]
2022-01-24 09:02:00.918 TRACE [...,true] 29314 --- [tp1324897979-85] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [TIMESTAMP] - [2021-12-23T13:00:00Z]
2022-01-24 09:02:00.920 TRACE [...,true] 29314 --- [tp1324897979-85] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [TIMESTAMP] - [2021-12-23T15:10:00Z]
正如您在堆栈跟踪中看到的那样,该错误与您提供的值 2021-12-23T13:00
到 Instant
之间的转换有关,可能是在 Spring 数据执行 JPA Criteria 查询时您在 Specification
.
要解决此问题,您可以尝试在 Specification
代码中手动将 LocalDateTime
转换为 Instant
- 我假设 transactionSearchCriteria.getFromDate()
和 transactionSearchCriteria.getToDate()
是 String
。例如:
public class TransactionSearchSpecification {
private static final String AMD = "amd";
private static final String CREATED_DATE = "createdDate";
public static Specification<Transaction> getTransactionBySpecification(
TransactionSearchCriteria transactionSearchCriteria) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if( !CollectionUtils.isEmpty( transactionSearchCriteria.getAmdList() ) ) {
predicates.add( root.get( SKU ).in( transactionSearchCriteria.getAmdList() ) );
}
String fromDate = transactionSearchCriteria.getFromDate();
String toDate = transactionSearchCriteria.getToDate();
if( !StringUtils.isEmpty( fromDate ) && !StringUtils.isEmpty(
toDate ) {
try {
Instant fromInstant = this.fromString(fromDate);
Instant toInstant = this.fromString(toDate);
predicates.add(
criteriaBuilder.between(
root.get( CREATED_DATE ), fromInstant, toInstant
)
);
} catch (DateTimeParseException dtpe) {
// invalid format, consider log the error, etcetera
dtpe.printStackTrace();
}
}
return criteriaBuilder.and( predicates.toArray( new Predicate[ 0 ] ) );
};
}
// This method is suitable for being defined probably in a common, utility class
// Following, for example, the advice provided in this SO question
//
private Instant fromString(final String localDateTimeString) throws DateTimeParseException {
if (StringUtils.isEmpty(localDateTimeString) {
return null;
}
// Convert String to LocalDateTime
LocalDateTime localDateTime = LocalDateTime.parse(
localDateTimeString,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm" , Locale.US )
);
// Provide the appropriate ZoneId
Instant result = localDateTime
.atZone(ZoneId.of("Asia/Kolkata"))
.toInstant();
// Return the obtained instant
result;
}
}
我不清楚 RetrieveProductMovementHistoryRequest
的作用。如果 Specification
以任何方式涉及使用 LocalDateTime fromCreatedDate
,那么我的建议是您需要手动调整 Specification
中的代码来处理 [=13] =] 值。我的意思是:
public class TransactionSearchSpecification {
private static final String AMD = "amd";
private static final String CREATED_DATE = "createdDate";
public static Specification<Transaction> getTransactionBySpecification(
TransactionSearchCriteria transactionSearchCriteria) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if( !CollectionUtils.isEmpty( transactionSearchCriteria.getAmdList() ) ) {
predicates.add( root.get( SKU ).in( transactionSearchCriteria.getAmdList() ) );
}
LocalDateTime fromDate = transactionSearchCriteria.getFromDate();
LocalDateTime toDate = transactionSearchCriteria.getToDate();
if( fromDate != null && toDate != null ) {
Instant fromInstant = this.fromLocalDateTime(fromDate);
Instant toInstant = this.fromLocalDateTime(toDate);
predicates.add(
criteriaBuilder.between(
root.get( CREATED_DATE ), fromInstant, toInstant
)
);
}
return criteriaBuilder.and( predicates.toArray( new Predicate[ 0 ] ) );
};
}
// Again, this method is suitable for being defined probably in a common, utility class
private Instant fromLocalDateTime(final LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
// Provide the appropriate ZoneId
Instant result = localDateTime
.atZone(ZoneId.of("Asia/Kolkata"))
.toInstant();
// Return the obtained instant
result;
}
}
对于您的评论,您似乎没有得到任何结果。
第一步,尝试调试和隔离错误:务必检查您的存储库是否实际返回任何结果。
如果它从数据库返回适当的记录,请检查您的服务代码,即与 Specification
交互的代码。
如果您的存储库没有返回任何结果,则问题可能与不同的事情有关。
请注意,主要是我们使用的是时间点。正如您在 MySql documentation 中看到的那样,根据不同的因素,尤其是 CREATED_DATE
列的类型,TIMESTAMP
或 DATETIME
,您可能会获得意想不到的结果执行查询时的结果,这可以解释为什么您没有获得任何结果。
我发现了问题,这可能是 JPA 如何与 MySQL 或 MySQL 本身交互的一种方式。问题是 fromCreatedDate
和 toCreatedDate
在执行时被转换为 localDateTime
。例如,我发送的日期是即时 2021-12-20T13:00:41Z
和 2021-12-29T15:10:08Z
,它被转换为阿联酋本地时间 2021-12-23 17:00:41.0
和 2021-12-23 19:10:08.0
。所以代码工作正常,但因为那天没有记录。我给了我空结果。
我启用 MySql 常规日志以查看实际查询到 MySql 的查询。我参考了 this 视频以在 MySql 中启用日志。