有没有一种方法可以检查JPQL中的空ZonedDateTime?

时间:2019-09-07 14:02:10

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

在一些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

如果不检查null,则会引发以下异常:

Caused by: org.postgresql.util.PSQLException: 
ERROR: could not determine data type of parameter $1

我通过使用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. 字符串在Spring Data存储库中使用此查询。函数@Query-只是PostgreSQL函数truncate_for_minute(...)的一个小定制。

我知道我可以实现自定义存储库并动态构建查询字符串,但是为什么不能在JPQL字符串中检查date_trunc(...) null?也许有办法做到这一点?

我的环境:

  • Java 11
  • x86_64-pc-linux-musl上的PostgreSQL 11.3,由gcc(Alpine 8.3.0)8.3.0,64位编译
  • 休眠5.3.7最终版

2 个答案:

答案 0 :(得分:0)

另一种解决方案是使用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;

答案 1 :(得分:0)

就我而言,问题如下。我注册了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)"
            )
        );
    }
}