6

Spring Boot 3 (Hibernate 6.1) in combination with PostgreSQL seems to have a problem with Instant values mapped to SQL timestamps (without time zone), as the following test case fails. Values read differ from the written values by the amount of the local time zone offset.

The test case executes successfully when using the H2 test database or when switching back to Spring Boot 2.7.6 (Hibernate 5.6).

JPA entity:

@Entity
public class MyEntity {

  @Id
  UUID id = UUID.randomUUID();

  //@JdbcType(TimestampJdbcType.class) //work-around to get it somehow working 
                                       //(alternative: declare this globally in a custom Hibernate dialect)
                                       //but why is this neccessary only with spring-boot-3 and postgres?
  @Column(columnDefinition = "timestamp")
  Instant createdTimestamp = Instant.now();
}

Spring Data repository:

public interface MyEntityRepository extends CrudRepository<MyEntity, UUID> {
}

JUnit integration test:

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) //comment out to use the H2 database
class MyEntityRepositoryITest {

  @Autowired
  MyEntityRepository myEntityRepository;

  @Test
  @Transactional(propagation = NOT_SUPPORTED) //do not use a transaction
  void readInstantSameAsPersisted() {
    var entity = new MyEntity();
    myEntityRepository.save(entity);

    var read = myEntityRepository.findById(entity.id).orElseThrow();
    assertEquals(entity.createdTimestamp.getEpochSecond(), read.createdTimestamp.getEpochSecond());
  }
}

Maven pom.xml:

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>test</scope>
</dependency>

Spring application.properties:

spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:postgresql://localhost:5432/dbname
spring.datasource.username=username
spring.datasource.password=password

Notes:

  • Setting spring.jpa.properties.hibernate.jdbc.time_zone to any value doesn't help.
  • I don't see switching to LocalDateTime as an option, as I want the createdTimestamp to represent an unambiguous point in time.
  • I don't see switching to a SQL type with zone information as an option, as I don't want to allow values with varying zones in the database.
  • spring.datasource.hikari.connection-init-sql=SET TIME ZONE 'UTC' also works as a work-around, but I don't see why this is neccessary -- reading a value that is based on seconds since 1970-01-01 should give the same result as the written value, regardless of which zone is used behind the scenes.
  • pgJDBC does not support Instant, but Hibernate does.
  • This answer could be related, but then why does it work with the older Hibernate?

Update

This answer says timestamp with time zone does (unlike the name suggests) in fact not carry additional zone information and just "stores a point on the UTC time line" that can be represented in different zones, which seems like a better fit for my use case.

Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
Knight Industries
  • 1,325
  • 10
  • 20
  • If you want to represent a moment, a specific point on the time line, then yes you must define your column as type `TIMESTAMP WITH TIME ZONE`. Postgres automatically uses any time zone or offset info accompanying an input to adjust the value into UTC (an offset from UTC of zero hours-minutes-seconds). The stored values in such a column always have an offset of zero, *always*. Some tools unfortunately inject a default time zone upon retrieval creating the illusion of a stored time zone where actually none exists. – Basil Bourque Dec 20 '22 at 22:38
  • I do not know Hibernate — perhaps it can support `Instant` in some fashion, I don't know. But I do know that JDBC does *not* support `Instant`. JDBC 4.2+ requires support for a subset of *java.time* classes that map to the SQL Standard’s conception of date-time values (a rather wacky confused conception, by the way). That subset does *not* include `Instant`. To exchange a moment **through JDBC with a column of type `TIMESTAMP WITH TIME ZONE`, use only the type `OffsetDateTime`**. After retrieval from Postgres, your Java code can extract an `Instant`, or apply a `ZoneId` for a `ZonedDateTime`. – Basil Bourque Dec 20 '22 at 22:40
  • @BasilBourque Hibernate handles this conversion as I saw digging through its source, and this is okay as `OffsetDateTime` also represents an unambiguously defined moment in time. – Knight Industries Dec 20 '22 at 23:06
  • Is this your final solution ? spring.datasource.hikari.connection-init-sql=SET TIME ZONE 'UTC' – Holm Jan 25 '23 at 16:16
  • No, this was the intermediate solution. Final solution is to switch to `timestamp with time zone`. – Knight Industries Jan 26 '23 at 17:53

1 Answers1

4

The PostgreSQL documentation says:

For timestamp with time zone, the internally stored value is always in UTC... An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone.

If you declare the field as timestamp with time zone, or briefly as timestamptz, then you will reach exactly what you want. It will also fit your requirement not to have multiple timezones in the database.

mentallurg
  • 4,967
  • 5
  • 28
  • 36