【问题标题】:Is there a way to check for null ZonedDateTime in JPQL?有没有办法在 JPQL 中检查 null ZonedDateTime?
【发布时间】: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 可能是相关的。据此,我认为不,这是不可能的。

标签: java postgresql spring-data-jpa spring-data jpql


【解决方案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)"
                )
            );
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2015-05-12
      • 1970-01-01
      • 2015-08-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-06
      • 2021-09-26
      相关资源
      最近更新 更多