Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 731
Sebastiaan van den Broek Avatar asked Nov 27 '18 12:11

Sebastiaan van den Broek


People also ask

What is Date_trunc in Postgres?

In PostgreSQL, DATE_TRUNC Function is used to truncate a timestamp type or interval type with specific and high level of precision. Syntax: date_trunc('datepart', field) The datepart argument in the above syntax is used to truncate one of the field,below listed field type: millennium. century.

What does Date_trunc return?

Returns timestamp truncated to the unit specified in field .

What is Spring Data JPA?

Spring Data JPA aims to significantly improve the implementation of data access layers by reducing the effort to the amount that's actually needed. As a developer you write your repository interfaces, including custom finder methods, and Spring will provide the implementation automatically.

Does JPA support instant?

JPA supports LocalDate , LocalTime , LocalDateTime etc. but not Instant.


2 Answers

As explained in the Hibernate 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")
);

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:

  • DateTruncUtcFunctionTest
  • DateTruncTimeZoneFunctionTest
like image 95
Vlad Mihalcea Avatar answered Sep 22 '22 04:09

Vlad Mihalcea


You can simply call a postgres function using the following syntax -

function('date_trunc', 'month', date)

As an example -

select distinct w from YourModel w where studentId=:studentId and function('date_trunc', 'month', date) = :month
like image 43
karthiks3000 Avatar answered Sep 21 '22 04:09

karthiks3000