有没有办法在 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 字符串中的nullZonedDateTime?也许有办法做到这一点?

我的环境:

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 区域不可知

如何在 JPQL 中检查 UUID 空值?

JPQL 在@Query 注释中检查大于小于今天的日期

当整列为 0 或 null 时,有没有办法可以隐藏 Oracle View 数据透视表中的列?

直接在 JPA / JPQL 中将行插入连接表?

有没有办法在 Apache Phoenix 中使用 TIME ZONE 信息 UPSERT 时间戳?