What is the JPQL equivalent of the PostgreSQL date_trunc('day', (entity.date AT TIME ZONE 'UTC')) function when using JPA and Hibernate

Sebastiaan van den Broek :

What is the JPQL/JPA/Hibernate equivalent of the database function date_trunc('day', (entity.date AT TIME ZONE 'UTC')) ?

I need to be rounding down to full days (and weeks, etc.) in a Spring Boot application with Hibernate running on top of a Postgresql database. This query is possible in native postgresql but I haven't figured out how to do it in Hibernate yet. The timezone (like 'UTC', 'Europe/Amsterdam') and date part (like 'day', 'year') may vary so I can't just set it to a default.

I found an ancient post at the Hibernate forums without any replies: https://forum.hibernate.org/viewtopic.php?f=1&t=990451

I also found a related StackOverflow question where the poster directly wants to select on a date as a certain timezone. It has no replies though: HQL equivalent of Postgres' "datetime at time zone"

My whole query (without the where) looks like this so a different way to achieve this would be ok too.

"SELECT NEW " + AggregateQueryEntity.class.getName() +
                "(date_trunc('" + aggregationPeriod.toString() +
                "', a.date), a.alertConfiguration.id.id, a.alertConfiguration.name, a.alertLevel, count(*)) from Alert a" +
                whereClause.toString() +
                " GROUP BY 1, a.alertConfiguration.id.id, a.alertConfiguration.name, a.alertLevel" +    //column 1 is the truncated date
                " ORDER BY 1, alertLevel DESC"

Edit: comments on answer.

Vlad's answer is great, I adapted it some more to also have the date part as a variable. Additionally I had to override the standard Hibernate behavior which generates Date columns as timestamp without timezone and explicitly make them timestamp with timezone by putting this on my column definition:

@Column(columnDefinition = "TIMESTAMP WITH TIME ZONE") private Date date;

For further reading, see https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_timestamp_.28without_time_zone.29

It's also still a good idea to use the Hibernate property hibernate.jdbc.time_zone, according to Vlad's other blog post https://vladmihalcea.com/how-to-store-date-time-and-timestamps-in-utc-time-zone-with-jdbc-and-hibernate/ and start your JVM in UTC. I had to do this too in order to fix all my timezone issues.

Vlad Mihalcea :

This is a very interesting question, so I decided to turn in into an article.

As explained in the User Guide, yu can use either the EXTRACT function:

List<Integer> days = entityManager
.createQuery(
    "select extract( day from c.timestamp ) " +
    "from Call c ", Integer.class )
.getResultList();

or the day function:

List<Integer> days = entityManager
.createQuery(
    "select day( c.timestamp ) " +
    "from Call c ", Integer.class )
.getResultList();

Registering the DATE_TRUNC function using the MetadataBuilderContributor

If you need to use the AT TIMEZONE as well, you need to register the PostgreSQL function like this:

public class SqlFunctionsMetadataBuilderContributor
        implements MetadataBuilderContributor {

    @Override
    public void contribute(MetadataBuilder metadataBuilder) {
        metadataBuilder.applySqlFunction(
                "date_trunc",
                new SQLFunctionTemplate(
                        StandardBasicTypes.TIMESTAMP,
                        "date_trunc('day', (?1 AT TIME ZONE 'UTC'))"
                )
        );
    }
}

Configuring the MetadataBuilderContributor using JPA persistence.xml

Now, you need to provide the SqlFunctionsMetadataBuilderContributor to Hibernate via the hibernate.metadata_builder_contributor configuration property:

<property>
    name="hibernate.metadata_builder_contributor" 
    value="com.vladmihalcea.book.hpjp.hibernate.query.function.SqlFunctionsMetadataBuilderContributor"
</property>

Configuring the MetadataBuilderContributor using Spring Boot

If you're using Spring Boot, you need to add the following configuration to the application.properties file:

spring.jpa.properties.hibernate.metadata_builder_contributor=com.vladmihalcea.book.hpjp.hibernate.query.function.SqlFunctionsMetadataBuilderContributor

Testing data

Then, assuming you have the following Post entity in the database:

Post post = new Post();
post.setId(1L);
post.setTitle(
    "High-Performance Java Persistence"
);
post.setCreatedOn(
    Timestamp.valueOf(
        LocalDateTime.of(2018, 11, 23, 11, 22, 33)
    )
);

entityManager.persist(post);

Using the DATE_TRUNC in JPQL

You can call the date_trunc function in JPQL like this:

Tuple tuple = entityManager
.createQuery(
    "select p.title as title, date_trunc(p.createdOn) as creation_date " +
    "from Post p " +
    "where p.id = :postId", Tuple.class)
.setParameter("postId", 1L)
.getSingleResult();

assertEquals(
    "High-Performance Java Persistence", 
    tuple.get("title")
);

assertEquals(
    Timestamp.valueOf(
        LocalDateTime.of(2018, 11, 23, 0, 0, 0)
    ), 
    tuple.get("creation_date")
);

For more details, check out this article.

Customizing the time zone passed to DATE_TRUNC

If you want to customize the time zone, just pass the time zone as a parameter like this:

public static class SqlFunctionsMetadataBuilderContributor
        implements MetadataBuilderContributor {

    @Override
    public void contribute(MetadataBuilder metadataBuilder) {
        metadataBuilder.applySqlFunction(
                "date_trunc",
                new SQLFunctionTemplate(
                        StandardBasicTypes.TIMESTAMP,
                        "date_trunc('day', (?1 AT TIME ZONE ?2))"
                )
        );
    }
}

Now, you can call the date_trunc function as follows:

Tuple tuple = entityManager
.createQuery(
    "select p.title as title, date_trunc(p.createdOn, :timezone) as creation_date " +
    "from Post p " +
    "where p.id = :postId", Tuple.class)
.setParameter("postId", 1L)
.setParameter("timezone", "UTC")
.getSingleResult();

Test cases

I created the following test cases in my high-performance-java-persistence GitHub repository, and they run just fine:

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=90074&siteId=1