有没有办法在 JPQL 中检查 null ZonedDateTime?
Posted
技术标签:
【中文标题】有没有办法在 JPQL 中检查 null ZonedDateTime?【英文标题】:Is there a way to check for null ZonedDateTime in JPQL? 【发布时间】:2020-01-10 01:30:17 【问题描述】:在一些 E2E 测试中,我遇到了问题。假设,我有以下 JPQL 查询:
Query query = entityManager.createQuery(
" select d from Document d left join d.audit da " +
" where " +
" (cast(:startDate as java.time.ZonedDateTime)) is null " +
" or truncate_for_minutes(da.dateCreate, 'UTC') >= " +
" truncate_for_minutes(:startDate, 'UTC')")
.setParameter("startDate", ZonedDateTime.now());
在查询字符串中,我使用命名参数startDate
,它可以是null
。上面的查询有效。但是如果我通过null
,则会抛出以下异常:
Caused by: org.postgresql.util.PSQLException:
ERROR: cannot cast type bytea to timestamp without time zone
如果不进行类型转换,则会引发以下异常:
Caused by: org.postgresql.util.PSQLException:
ERROR: could not determine data type of parameter $1
如果不检查 null,则会引发以下异常:
Caused by: org.postgresql.util.PSQLException:
ERROR: function pg_catalog.timezone(unknown, bytea) does not exist
No function matches the given name and argument types.
You might need to add explicit type casts.
我通过 @Query
字符串在 Spring Data 存储库中使用此查询。函数 truncate_for_minute(...)
- 只是对 PostgreSQL 函数 date_trunc(...)
的一个小定制。
我知道我可以实现自定义存储库并动态构建查询字符串,但是为什么我不能检查 JPQL 字符串中的null
ZonedDateTime
?也许有办法做到这一点?
我的环境:
Java 11 x86_64-pc-linux-musl 上的 PostgreSQL 11.3,由 gcc (Alpine 8.3.0) 8.3.0 编译,64 位 休眠 5.3.7 最终版【问题讨论】:
反问:如果参数是null
,你希望发生什么?
This question 可能是相关的。据此,我认为不,这是不可能的。
【参考方案1】:
另一种解决方案是使用Criteria API 并动态构建查询。
@Repository
@RequiredArgsConstructor
public class CustomDocumentRepositoryImpl implements CustomDocumentRegistry
private final EntityManager em;
@Override
public Page<Document> findDocumentsForExpertByFilter(SearchDocumentCriteria criteria,
Pageable pageable)
final String AUDIT_TABLE = "...";
final String USER_TABLE = "...";
final String ID_FIELD = "id";
final String FIRST_NAME_FIELD = "...";
final String LAST_NAME_FIELD = "...";
final String MIDDLE_NAME_FIELD = "...";
final String WORKSTATION_FIELD = "...";
final String DATE_CREATE_FIELD = "...";
final String LIKE_MASK = "%%%s%%";
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Document> query = cb.createQuery(Document.class);
Root<Document> root = query.from(Document.class);
Path<ZonedDateTime> dateCreatePath =
root.get(AUDIT_TABLE).get(DATE_CREATE_FIELD);
Path<String> lastNamePath =
root.get(AUDIT_TABLE).get(USER_TABLE).get(LAST_NAME_FIELD);
Path<String> firstNamePath =
root.get(AUDIT_TABLE).get(USER_TABLE).get(FIRST_NAME_FIELD);
Path<String> middleNamePath =
root.get(AUDIT_TABLE).get(USER_TABLE).get(MIDDLE_NAME_FIELD);
root.fetch(AUDIT_TABLE, JoinType.LEFT)
.fetch(USER_TABLE, JoinType.LEFT);
Predicate documentIdsPredicate;
List<Long> documentIds = criteria.getIds();
if (isNull(documentIds) || documentIds.isEmpty())
documentIdsPredicate = cb.isNotNull(root.get(ID_FIELD));
else
documentIdsPredicate = root.get(ID_FIELD).in(criteria.getIds());
Predicate startDatePredicate;
ZonedDateTime startDate = criteria.getStartDate();
if (isNull(startDate))
startDatePredicate = cb.isNotNull(dateCreatePath);
else
startDatePredicate = cb.greaterThanOrEqualTo(dateCreatePath, startDate);
Predicate endDatePredicate;
ZonedDateTime endDate = criteria.getEndDate();
if (isNull(endDate))
endDatePredicate = cb.isNotNull(dateCreatePath);
else
endDatePredicate = cb.lessThanOrEqualTo(dateCreatePath, endDate);
Predicate lastNamePredicate = cb.like(cb.upper(lastNamePath),
format(LIKE_MASK, criteria.getLastName().toUpperCase()));
Predicate firstNamePredicate = cb.like(cb.upper(firstNamePath),
format(LIKE_MASK, criteria.getFirstName().toUpperCase()));
Predicate middleNamePredicate = cb.like(cb.upper(middleNamePath),
format(LIKE_MASK, criteria.getMiddleName().toUpperCase()));
Predicate fullNamePredicate =
cb.and(lastNamePredicate, firstNamePredicate, middleNamePredicate);
Predicate compositePredicate = cb.and(
fullNamePredicate,
documentIdsPredicate,
startDatePredicate,
endDatePredicate
);
query.where(compositePredicate);
Query limitedQuery = em.createQuery(query
.orderBy(cb.desc(root.get(AUDIT_TABLE).get(DATE_CREATE_FIELD))))
.setFirstResult(nonNull(criteria.getSize()) ?
criteria.getPage() * criteria.getSize() :
criteria.getPage());
if (nonNull(criteria.getSize()))
limitedQuery.setMaxResults(criteria.getSize());
List<Document> documents = limitedQuery.getResultList();
return new PageImpl<>(documents, pageable, criteria.getSize());
生成以下 SQL:
select
document0_.id as id1_3_0_,
user1_.id as id1_13_1_,
document0_.created_dt as created_2_3_0_,
document0_.updated_dt as updated_3_3_0_,
document0_.created_user_id as created_6_3_0_,
document0_.updated_user_id as updated_7_3_0_,
document0_.name as name4_3_0_,
user1_.first_name as first_na2_13_1_,
user1_.last_name as last_nam3_13_1_,
user1_.middle_name as middle_n5_13_1_
from
some_scheme.document document0_
left outer join
some_scheme.s_user user1_
on document0_.created_user_id=user1_.id cross
join
some_scheme.s_user user2_
where
document0_.created_user_id=user2_.id
and (
upper(user2_.last_name) like '%LASTNAME%'
)
and (
upper(user2_.first_name) like '%FIRSTNAME%'
)
and (
upper(user2_.middle_name) like '%MIDDLENAME%'
)
and (
document0_.id in (
2 , 1
)
)
and document0_.created_dt>=...
and document0_.created_dt<=...
order by
document0_.created_dt desc limit 10;
【讨论】:
【参考方案2】:就我而言,问题如下。我注册了自定义SQL函数date_trunc
:
public class CustomSqlFunction implements MetadataBuilderContributor
@Override
public void contribute(MetadataBuilder metadataBuilder)
metadataBuilder.applySqlFunction(
"truncate_for_minutes",
new SQLFunctionTemplate(
StandardBasicTypes.TIMESTAMP,
"date_trunc('minute', (?1 AT TIME ZONE ?2))"
)
);
如果将StandardBasicTypes.TIMESTAMP
更改为ZonedDateTime.INSTANCE
并在一个参数中传递ZonedDateTime
,则JPQL查询中的比较不会导致错误:
public class CustomSqlFunction implements MetadataBuilderContributor
@Override
public void contribute(MetadataBuilder metadataBuilder)
metadataBuilder.applySqlFunction(
"truncate_for_minutes",
new SQLFunctionTemplate(
ZonedDateTimeType.INSTANCE,
"date_trunc('minute', ?1)"
)
);
【讨论】:
以上是关于有没有办法在 JPQL 中检查 null ZonedDateTime?的主要内容,如果未能解决你的问题,请参考以下文章
有没有办法让 jpql/native-query 区域不可知