Skip to content

Commit

Permalink
Merge pull request #31834 from yrodiere/i31586-tz-storage
Browse files Browse the repository at this point in the history
Introduce `quarkus.hibernate-orm.mapping.timezone.default-storage`
  • Loading branch information
gsmet authored Mar 14, 2023
2 parents d3f527c + 51981f7 commit 912fc9e
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.util.OptionalLong;
import java.util.Set;

import org.hibernate.annotations.TimeZoneStorageType;

import io.quarkus.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigGroup;
Expand Down Expand Up @@ -154,6 +156,13 @@ public class HibernateOrmConfigPersistenceUnit {
@ConvertWith(TrimmedStringConverter.class)
public Optional<Set<String>> mappingFiles;

/**
* Mapping configuration.
*/
@ConfigItem
@ConfigDocSection
public HibernateOrmConfigPersistenceUnitMapping mapping;

/**
* Query related configuration.
*/
Expand Down Expand Up @@ -265,6 +274,7 @@ public boolean isAnyPropertySet() {
physicalNamingStrategy.isPresent() ||
implicitNamingStrategy.isPresent() ||
metadataBuilderContributor.isPresent() ||
mapping.isAnyPropertySet() ||
query.isAnyPropertySet() ||
database.isAnyPropertySet() ||
jdbc.isAnyPropertySet() ||
Expand Down Expand Up @@ -324,6 +334,58 @@ public boolean isAnyPropertySet() {
}
}

/**
* Mapping-related configuration.
*/
@ConfigGroup
public static class HibernateOrmConfigPersistenceUnitMapping {
/**
* How to store timezones in the database by default
* for properties of type `OffsetDateTime` and `ZonedDateTime`.
*
* This default may be overridden on a per-property basis using `@TimeZoneStorage`.
*
* NOTE: Properties of type `OffsetTime` are https://hibernate.atlassian.net/browse/HHH-16287[not affected by this
* setting].
*
* `default`::
* Equivalent to `native` if supported, `normalize-utc` otherwise.
* `auto`::
* Equivalent to `native` if supported, `column` otherwise.
* `native`::
* Stores the timestamp and timezone in a column of type `timestamp with time zone`.
* +
* Only available on some databases/dialects;
* if not supported, an exception will be thrown during static initialization.
* `column`::
* Stores the timezone in a separate column next to the timestamp column.
* +
* Use `@TimeZoneColumn` on the relevant entity property to customize the timezone column.
* `normalize-utc`::
* Does not store the timezone, and loses timezone information upon persisting.
* +
* Instead, normalizes the value to a timestamp in the UTC timezone.
* `normalize`::
* Does not store the timezone, and loses timezone information upon persisting.
* +
* Instead, normalizes the value:
* * upon persisting to the database, to a timestamp in the JDBC timezone
* set through `quarkus.hibernate-orm.jdbc.timezone`,
* or the JVM default timezone if not set.
* * upon reading back from the database, to the JVM default timezone.
* +
* Use this to get the legacy behavior of Quarkus 2 / Hibernate ORM 5 or older.
*
* @asciidoclet
*/
@ConfigItem(name = "timezone.default-storage", defaultValueDocumentation = "default")
public Optional<TimeZoneStorageType> timeZoneDefaultStorage;

public boolean isAnyPropertySet() {
return timeZoneDefaultStorage.isPresent();
}
}

@ConfigGroup
public static class HibernateOrmConfigPersistenceUnitQuery {

Expand Down Expand Up @@ -398,6 +460,8 @@ public static class HibernateOrmConfigPersistenceUnitJdbc {

/**
* The time zone pushed to the JDBC driver.
*
* See `quarkus.hibernate-orm.mapping.timezone.default-storage`.
*/
@ConfigItem
@ConvertWith(TrimmedStringConverter.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,12 @@ private static void producePersistenceUnitDescriptorFromConfig(
className -> descriptor.getProperties()
.setProperty(EntityManagerFactoryBuilderImpl.METADATA_BUILDER_CONTRIBUTOR, className));

// Mapping
if (persistenceUnitConfig.mapping.timeZoneDefaultStorage.isPresent()) {
descriptor.getProperties().setProperty(AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
persistenceUnitConfig.mapping.timeZoneDefaultStorage.get().name());
}

//charset
descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_CHARSET_NAME,
persistenceUnitConfig.database.charset.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import jakarta.persistence.EntityManagerFactory;

import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.metamodel.mapping.SelectableConsumer;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.EntityPersister;

public final class SchemaUtil {

Expand All @@ -27,4 +31,23 @@ public static Set<String> getColumnNames(EntityManagerFactory entityManagerFacto
}
return result;
}

public static String getColumnTypeName(EntityManagerFactory entityManagerFactory, Class<?> entityType,
String columnName) {
MappingMetamodel domainModel = entityManagerFactory
.unwrap(SessionFactoryImplementor.class).getRuntimeMetamodels().getMappingMetamodel();
EntityPersister entityDescriptor = domainModel.findEntityDescriptor(entityType);
var columnFinder = new SelectableConsumer() {
private SelectableMapping found;

@Override
public void accept(int selectionIndex, SelectableMapping selectableMapping) {
if (found == null && selectableMapping.getSelectableName().equals(columnName)) {
found = selectableMapping;
}
}
};
entityDescriptor.forEachSelectable(columnFinder);
return columnFinder.found.getJdbcMapping().getJdbcType().getFriendlyName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.quarkus.hibernate.orm.mapping;

import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

import jakarta.inject.Inject;

import org.assertj.core.api.SoftAssertions;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

import io.quarkus.narayana.jta.QuarkusTransaction;

public class AbstractTimezoneDefaultStorageTest {

private static final LocalDateTime LOCAL_DATE_TIME_TO_TEST = LocalDateTime.of(2017, Month.NOVEMBER, 6, 19, 19, 0);
public static final ZonedDateTime PERSISTED_ZONED_DATE_TIME = LOCAL_DATE_TIME_TO_TEST.atZone(ZoneId.of("Africa/Cairo"));
public static final OffsetDateTime PERSISTED_OFFSET_DATE_TIME = LOCAL_DATE_TIME_TO_TEST.atOffset(ZoneOffset.ofHours(3));
public static final OffsetTime PERSISTED_OFFSET_TIME = LOCAL_DATE_TIME_TO_TEST.toLocalTime()
.atOffset(ZoneOffset.ofHours(3));

@Inject
SessionFactory sessionFactory;

@Inject
Session session;

protected long persistWithValuesToTest() {
return QuarkusTransaction.requiringNew().call(() -> {
var entity = new EntityWithTimezones(PERSISTED_ZONED_DATE_TIME, PERSISTED_OFFSET_DATE_TIME);
session.persist(entity);
return entity.id;
});
}

protected void assertLoadedValues(long id, ZonedDateTime expectedZonedDateTime, OffsetDateTime expectedOffsetDateTime) {
QuarkusTransaction.requiringNew().run(() -> {
var entity = session.find(EntityWithTimezones.class, id);
SoftAssertions.assertSoftly(assertions -> {
assertions.assertThat(entity).extracting("zonedDateTime").isEqualTo(expectedZonedDateTime);
assertions.assertThat(entity).extracting("offsetDateTime").isEqualTo(expectedOffsetDateTime);
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.hibernate.orm.mapping;

import java.time.OffsetDateTime;
import java.time.ZonedDateTime;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class EntityWithTimezones {

@Id
@GeneratedValue
Long id;

public EntityWithTimezones() {
}

public EntityWithTimezones(ZonedDateTime zonedDateTime, OffsetDateTime offsetDateTime) {
this.zonedDateTime = zonedDateTime;
this.offsetDateTime = offsetDateTime;
}

public ZonedDateTime zonedDateTime;

public OffsetDateTime offsetDateTime;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.hibernate.orm.mapping;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.SchemaUtil;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.test.QuarkusUnitTest;

public class TimezoneDefaultStorageAutoTest extends AbstractTimezoneDefaultStorageTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(EntityWithTimezones.class)
.addClasses(SchemaUtil.class, SmokeTestUtils.class))
.withConfigurationResource("application.properties")
.overrideConfigKey("quarkus.hibernate-orm.mapping.timezone.default-storage", "auto");

@Test
public void schema() throws Exception {
assertThat(SchemaUtil.getColumnNames(sessionFactory, EntityWithTimezones.class))
.doesNotContain("zonedDateTime_tz", "offsetDateTime_tz");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "zonedDateTime"))
.isEqualTo("TIMESTAMP_WITH_TIMEZONE");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "offsetDateTime"))
.isEqualTo("TIMESTAMP_WITH_TIMEZONE");
}

@Test
public void persistAndLoad() {
long id = persistWithValuesToTest();
// For some reason native storage (with H2 at least) preserves the offset, but not the zone ID.
assertLoadedValues(id, PERSISTED_ZONED_DATE_TIME.withZoneSameInstant(PERSISTED_ZONED_DATE_TIME.getOffset()),
PERSISTED_OFFSET_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.hibernate.orm.mapping;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.SchemaUtil;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.test.QuarkusUnitTest;

public class TimezoneDefaultStorageColumnTest extends AbstractTimezoneDefaultStorageTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(EntityWithTimezones.class)
.addClasses(SchemaUtil.class, SmokeTestUtils.class))
.withConfigurationResource("application.properties")
.overrideConfigKey("quarkus.hibernate-orm.mapping.timezone.default-storage", "column");

@Test
public void schema() throws Exception {
assertThat(SchemaUtil.getColumnNames(sessionFactory, EntityWithTimezones.class))
.contains("zonedDateTime_tz", "offsetDateTime_tz")
// For some reason we don't get a TZ column for OffsetTime
.doesNotContain("offsetTime_tz");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "zonedDateTime"))
.isEqualTo("TIMESTAMP_UTC");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "offsetDateTime"))
.isEqualTo("TIMESTAMP_UTC");
}

@Test
public void persistAndLoad() {
long id = persistWithValuesToTest();
// For some reason column storage preserves the offset, but not the zone ID.
assertLoadedValues(id, PERSISTED_ZONED_DATE_TIME.withZoneSameInstant(PERSISTED_ZONED_DATE_TIME.getOffset()),
PERSISTED_OFFSET_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.hibernate.orm.mapping;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.SchemaUtil;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.test.QuarkusUnitTest;

public class TimezoneDefaultStorageDefaultTest extends AbstractTimezoneDefaultStorageTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(EntityWithTimezones.class)
.addClasses(SchemaUtil.class, SmokeTestUtils.class))
.withConfigurationResource("application.properties");

@Test
public void schema() throws Exception {
assertThat(SchemaUtil.getColumnNames(sessionFactory, EntityWithTimezones.class))
.doesNotContain("zonedDateTime_tz", "offsetDateTime_tz");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "zonedDateTime"))
.isEqualTo("TIMESTAMP_WITH_TIMEZONE");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "offsetDateTime"))
.isEqualTo("TIMESTAMP_WITH_TIMEZONE");
}

@Test
public void persistAndLoad() {
long id = persistWithValuesToTest();
// For some reason native storage (with H2 at least) preserves the offset, but not the zone ID.
assertLoadedValues(id, PERSISTED_ZONED_DATE_TIME.withZoneSameInstant(PERSISTED_ZONED_DATE_TIME.getOffset()),
PERSISTED_OFFSET_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.hibernate.orm.mapping;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.SchemaUtil;
import io.quarkus.hibernate.orm.SmokeTestUtils;
import io.quarkus.test.QuarkusUnitTest;

public class TimezoneDefaultStorageNativeTest extends AbstractTimezoneDefaultStorageTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(EntityWithTimezones.class)
.addClasses(SchemaUtil.class, SmokeTestUtils.class))
.withConfigurationResource("application.properties")
.overrideConfigKey("quarkus.hibernate-orm.mapping.timezone.default-storage", "native");

@Test
public void schema() throws Exception {
assertThat(SchemaUtil.getColumnNames(sessionFactory, EntityWithTimezones.class))
.doesNotContain("zonedDateTime_tz", "offsetDateTime_tz");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "zonedDateTime"))
.isEqualTo("TIMESTAMP_WITH_TIMEZONE");
assertThat(SchemaUtil.getColumnTypeName(sessionFactory, EntityWithTimezones.class, "offsetDateTime"))
.isEqualTo("TIMESTAMP_WITH_TIMEZONE");
}

@Test
public void persistAndLoad() {
long id = persistWithValuesToTest();
// For some reason native storage (with H2 at least) preserves the offset, but not the zone ID.
assertLoadedValues(id, PERSISTED_ZONED_DATE_TIME.withZoneSameInstant(PERSISTED_ZONED_DATE_TIME.getOffset()),
PERSISTED_OFFSET_DATE_TIME);
}
}
Loading

0 comments on commit 912fc9e

Please sign in to comment.