diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index 33b878aad4..fbd6ab712c 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -209,6 +209,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { private boolean collectionsInDefaultFetchGroupEnabled = true; private final boolean UnownedAssociationTransientCheck; private final boolean passProcedureParameterNames; + private final boolean preferJdbcDatetimeTypes; // JPA callbacks private final boolean callbacksEnabled; @@ -635,6 +636,12 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { configurationSettings, false ); + + this.preferJdbcDatetimeTypes = ConfigurationHelper.getBoolean( + AvailableSettings.NATIVE_PREFER_JDBC_DATETIME_TYPES, + configurationSettings, + false + ); } private boolean disallowBatchUpdates(Dialect dialect, ExtractedDatabaseMetaData meta) { @@ -1326,6 +1333,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { return passProcedureParameterNames; } + @Override + public boolean isPreferJdbcDatetimeTypesInNativeQueriesEnabled() { + return preferJdbcDatetimeTypes; + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // In-flight mutation access diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index cbe223cc2b..93256b10c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java @@ -512,4 +512,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp public boolean isPassProcedureParameterNames() { return delegate.isPassProcedureParameterNames(); } + + @Override + public boolean isPreferJdbcDatetimeTypesInNativeQueriesEnabled() { + return delegate.isPreferJdbcDatetimeTypesInNativeQueriesEnabled(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index 2a60d78149..27d2d4e335 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -365,4 +365,14 @@ public interface SessionFactoryOptions extends QueryEngineOptions { } boolean isPassProcedureParameterNames(); + + /** + * Should native queries return JDBC datetime types + * instead of using {@code java.time} types. + * + * @since 7.0 + * + * @see org.hibernate.cfg.QuerySettings#NATIVE_PREFER_JDBC_DATETIME_TYPES + */ + boolean isPreferJdbcDatetimeTypesInNativeQueriesEnabled(); } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java index 06ad3347ef..bf0fc95718 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java @@ -25,6 +25,7 @@ public interface QuerySettings { * @since 6.5 */ String PORTABLE_INTEGER_DIVISION = "hibernate.query.hql.portable_integer_division"; + /** * Specifies a {@link org.hibernate.query.hql.HqlTranslator} to use for HQL query * translation. @@ -135,15 +136,28 @@ public interface QuerySettings { String CRITERIA_COPY_TREE = "hibernate.criteria.copy_tree"; /** - * When set to true, indicates that ordinal parameters (represented by the '?' placeholder) in native queries will be ignored. - *

- * By default, this is set to false, i.e. native queries will be checked for ordinal placeholders. + * When enabled, ordinal parameters (represented by the {@code ?} placeholder) in + * native queries will be ignored. *

+ * By default, native queries are checked for ordinal placeholders. * * @see SessionFactoryOptions#getNativeJdbcParametersIgnored() */ String NATIVE_IGNORE_JDBC_PARAMETERS = "hibernate.query.native.ignore_jdbc_parameters"; + /** + * When enabled, native queries will return {@link java.sql.Date}, + * {@link java.sql.Time}, and {@link java.sql.Timestamp} instead of the + * datetime types from {@link java.time}, recovering the behavior of + * native queries in Hibernate 6 and earlier. + *

+ * By default, native queries return {@link java.time.LocalDate}, + * {@link java.time.LocalTime}, and {@link java.time.LocalDateTime}. + * + * @since 7.0 + */ + String NATIVE_PREFER_JDBC_DATETIME_TYPES = "hibernate.query.native.prefer_jdbc_datetime_types"; + /** * When {@linkplain org.hibernate.query.Query#setMaxResults(int) pagination} is used * in combination with a {@code fetch join} applied to a collection or many-valued diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/DateJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/DateJdbcType.java index d7cb8785fb..15eb1bbcf0 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/DateJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/DateJdbcType.java @@ -12,6 +12,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.time.LocalDate; import java.util.Calendar; import jakarta.persistence.TemporalType; @@ -54,7 +55,9 @@ public class DateJdbcType implements JdbcType { Integer length, Integer scale, TypeConfiguration typeConfiguration) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Date.class ); + return typeConfiguration.getCurrentBaseSqlTypeIndicators().preferJdbcDatetimeTypes() + ? typeConfiguration.getJavaTypeRegistry().getDescriptor( Date.class ) + : typeConfiguration.getJavaTypeRegistry().getDescriptor( LocalDate.class ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java index f83717d0ff..abc55badc0 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java @@ -9,7 +9,6 @@ package org.hibernate.type.descriptor.jdbc; import jakarta.persistence.EnumType; import jakarta.persistence.TemporalType; -import org.hibernate.AssertionFailure; import org.hibernate.Incubating; import org.hibernate.TimeZoneStorageStrategy; import org.hibernate.dialect.Dialect; @@ -199,7 +198,7 @@ public interface JdbcTypeIndicators { /** * Resolves the given type code to a possibly different type code, based on context. - * + *

* A database might not support a certain type code in certain scenarios like within a UDT * and has to resolve to a different type code in such a scenario. * @@ -210,6 +209,18 @@ public interface JdbcTypeIndicators { return jdbcTypeCode; } + /** + * Should native queries return JDBC datetime types + * instead of using {@code java.time} types. + * + * @since 7.0 + * + * @see org.hibernate.cfg.QuerySettings#NATIVE_PREFER_JDBC_DATETIME_TYPES + */ + default boolean preferJdbcDatetimeTypes() { + return false; + } + /** * Provides access to the {@link TypeConfiguration} for access to various type system related registries. */ @@ -221,47 +232,36 @@ public interface JdbcTypeIndicators { /** * @return the SQL column type used for storing times under the - * given {@linkplain TimeZoneStorageStrategy storage strategy} + * given {@linkplain TimeZoneStorageStrategy storage strategy} * * @see SqlTypes#TIME_WITH_TIMEZONE * @see SqlTypes#TIME * @see SqlTypes#TIME_UTC */ static int getZonedTimeSqlType(TimeZoneStorageStrategy storageStrategy) { - switch ( storageStrategy ) { - case NATIVE: - return SqlTypes.TIME_WITH_TIMEZONE; - case COLUMN: - case NORMALIZE: - return SqlTypes.TIME; - case NORMALIZE_UTC: - return SqlTypes.TIME_UTC; - default: - throw new AssertionFailure( "unknown time zone storage strategy" ); - } + return switch (storageStrategy) { + case NATIVE -> SqlTypes.TIME_WITH_TIMEZONE; + case COLUMN, NORMALIZE -> SqlTypes.TIME; + case NORMALIZE_UTC -> SqlTypes.TIME_UTC; + }; } /** * @return the SQL column type used for storing datetimes under the - * given {@linkplain TimeZoneStorageStrategy storage strategy} + * given {@linkplain TimeZoneStorageStrategy storage strategy} * * @see SqlTypes#TIME_WITH_TIMEZONE * @see SqlTypes#TIMESTAMP * @see SqlTypes#TIMESTAMP_UTC */ static int getZonedTimestampSqlType(TimeZoneStorageStrategy storageStrategy) { - switch ( storageStrategy ) { - case NATIVE: - return SqlTypes.TIMESTAMP_WITH_TIMEZONE; - case COLUMN: - case NORMALIZE: - return SqlTypes.TIMESTAMP; - case NORMALIZE_UTC: + return switch (storageStrategy) { + case NATIVE -> SqlTypes.TIMESTAMP_WITH_TIMEZONE; + case COLUMN, NORMALIZE -> SqlTypes.TIMESTAMP; + case NORMALIZE_UTC -> // sensitive to hibernate.type.preferred_instant_jdbc_type - return SqlTypes.TIMESTAMP_UTC; - default: - throw new AssertionFailure( "unknown time zone storage strategy" ); - } + SqlTypes.TIMESTAMP_UTC; + }; } /** @@ -286,17 +286,13 @@ public interface JdbcTypeIndicators { */ default int getDefaultZonedTimestampSqlType() { final TemporalType temporalPrecision = getTemporalPrecision(); - switch ( temporalPrecision == null ? TemporalType.TIMESTAMP : temporalPrecision ) { - case TIME: - return getZonedTimeSqlType( getDefaultTimeZoneStorageStrategy() ); - case DATE: - return Types.DATE; - case TIMESTAMP: + return switch (temporalPrecision == null ? TemporalType.TIMESTAMP : temporalPrecision) { + case TIME -> getZonedTimeSqlType( getDefaultTimeZoneStorageStrategy() ); + case DATE -> Types.DATE; + case TIMESTAMP -> // sensitive to hibernate.timezone.default_storage - return getZonedTimestampSqlType( getDefaultTimeZoneStorageStrategy() ); - default: - throw new IllegalArgumentException( "Unexpected jakarta.persistence.TemporalType : " + temporalPrecision); - } + getZonedTimestampSqlType( getDefaultTimeZoneStorageStrategy() ); + }; } Dialect getDialect(); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeJdbcType.java index 3cc324f555..65e3edb170 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeJdbcType.java @@ -12,6 +12,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Time; import java.sql.Types; +import java.time.LocalTime; import java.util.Calendar; import jakarta.persistence.TemporalType; @@ -54,7 +55,9 @@ public class TimeJdbcType implements JdbcType { Integer length, Integer scale, TypeConfiguration typeConfiguration) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Time.class ); + return typeConfiguration.getCurrentBaseSqlTypeIndicators().preferJdbcDatetimeTypes() + ? typeConfiguration.getJavaTypeRegistry().getDescriptor( Time.class ) + : typeConfiguration.getJavaTypeRegistry().getDescriptor( LocalTime.class ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampJdbcType.java index cecda2f094..c170a88997 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampJdbcType.java @@ -12,6 +12,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; +import java.time.LocalDateTime; import java.util.Calendar; import jakarta.persistence.TemporalType; @@ -54,7 +55,9 @@ public class TimestampJdbcType implements JdbcType { Integer length, Integer scale, TypeConfiguration typeConfiguration) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Timestamp.class ); + return typeConfiguration.getCurrentBaseSqlTypeIndicators().preferJdbcDatetimeTypes() + ? typeConfiguration.getJavaTypeRegistry().getDescriptor( Timestamp.class ) + : typeConfiguration.getJavaTypeRegistry().getDescriptor( LocalDateTime.class ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java index f24fd188d3..93aa5d3614 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java +++ b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java @@ -482,6 +482,12 @@ public class TypeConfiguration implements SessionFactoryObserver, Serializable { : sessionFactory.getJdbcServices().getDialect(); } + @Override + public boolean preferJdbcDatetimeTypes() { + return sessionFactory != null + && sessionFactory.getSessionFactoryOptions().isPreferJdbcDatetimeTypesInNativeQueriesEnabled(); + } + private Scope(TypeConfiguration typeConfiguration) { this.typeConfiguration = typeConfiguration; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryResultTypeAutoDiscoveryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryResultTypeAutoDiscoveryTest.java index ee9d14e78a..45cd0b25f4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryResultTypeAutoDiscoveryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryResultTypeAutoDiscoveryTest.java @@ -379,12 +379,11 @@ public class NativeQueryResultTypeAutoDiscoveryTest { private Map buildSettings(Class ... entityTypes) { Map settings = new HashMap<>(); - - settings.put( org.hibernate.cfg.AvailableSettings.HBM2DDL_AUTO, "create-drop" ); - settings.put( org.hibernate.cfg.AvailableSettings.DIALECT, DIALECT.getClass().getName() ); + settings.put( AvailableSettings.NATIVE_PREFER_JDBC_DATETIME_TYPES, "true" ); + settings.put( AvailableSettings.HBM2DDL_AUTO, "create-drop" ); + settings.put( AvailableSettings.DIALECT, DIALECT.getClass().getName() ); settings.put( AvailableSettings.LOADED_CLASSES, Arrays.asList( entityTypes ) ); ServiceRegistryUtil.applySettings( settings ); - return settings; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryWithDatetimesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryWithDatetimesTest.java new file mode 100644 index 0000000000..6b1711986b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryWithDatetimesTest.java @@ -0,0 +1,53 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.jpa.query; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgresPlusDialect; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +@Jpa(annotatedClasses = NativeQueryWithDatetimesTest.Datetimes.class) +public class NativeQueryWithDatetimesTest { + @SkipForDialect(dialectClass = PostgresPlusDialect.class) + @SkipForDialect(dialectClass = OracleDialect.class) + @Test void test(EntityManagerFactoryScope scope) { + scope.inTransaction(s -> s.persist(new Datetimes())); + Object[] result = scope.fromTransaction(s -> (Object[]) s.createNativeQuery("select ctime, cdate, cdatetime from tdatetimes", Object[].class).getSingleResult()); + assertInstanceOf(LocalTime.class, result[0]); + assertInstanceOf(LocalDate.class, result[1]); + assertInstanceOf(LocalDateTime.class, result[2]); +// result = scope.fromTransaction(s -> (Object[]) s.createNativeQuery("select current_time, current_date, current_timestamp from tdatetimes", Object[].class).getSingleResult()); +// assertInstanceOf(LocalTime.class, result[0]); +// assertInstanceOf(LocalDate.class, result[1]); +// assertInstanceOf(LocalDateTime.class, result[2]); + } + + @Entity @Table(name = "tdatetimes") + static class Datetimes { + @Id + long id; + @Column(nullable = false, name = "ctime") + LocalTime localTime = LocalTime.now(); + @Column(nullable = false, name = "cdate") + LocalDate localDate = LocalDate.now(); + @Column(nullable = false, name = "cdatetime") + LocalDateTime localDateTime = LocalDateTime.now(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java index ad98967e04..7df51ec641 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/DateArrayTest.java @@ -6,8 +6,6 @@ */ package org.hibernate.orm.test.type; -import java.sql.Date; -import java.sql.Timestamp; import java.time.LocalDate; import org.hibernate.dialect.Dialect; @@ -39,7 +37,6 @@ import jakarta.persistence.Table; import jakarta.persistence.TypedQuery; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.core.Is.is; /** @@ -164,16 +161,16 @@ public class DateArrayTest { assertThat( tuple[1], is( new Object[] { - new Timestamp( Date.valueOf( date1 ).getTime() ), - new Timestamp( Date.valueOf( date2 ).getTime() ), - new Timestamp( Date.valueOf( date3 ).getTime() ) + date1, + date2, + date3 } ) ); } else { assertThat( tuple[1], - is( new Date[] { Date.valueOf( date1 ), Date.valueOf( date2 ), Date.valueOf( date3 ) } ) + is( new LocalDate[] { date1, date2, date3 } ) ); } } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimeArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimeArrayTest.java index ba18c45a0f..eed3ddfc3a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimeArrayTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimeArrayTest.java @@ -6,8 +6,6 @@ */ package org.hibernate.orm.test.type; -import java.sql.Time; -import java.sql.Timestamp; import java.time.LocalTime; import org.hibernate.dialect.Dialect; @@ -158,16 +156,16 @@ public class TimeArrayTest { assertThat( tuple[1], is( new Object[] { - new Timestamp( Time.valueOf( time1 ).getTime() ), - new Timestamp( Time.valueOf( time2 ).getTime() ), - new Timestamp( Time.valueOf( time3 ).getTime() ) + time1, + time2, + time3 } ) ); } else { assertThat( tuple[1], - is( new Time[] { Time.valueOf( time1 ), Time.valueOf( time2 ), Time.valueOf( time3 ) } ) + is( new LocalTime[] { time1, time2, time3 } ) ); } } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimestampArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimestampArrayTest.java index 2961ac5a40..ef95ad363a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimestampArrayTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/TimestampArrayTest.java @@ -6,7 +6,6 @@ */ package org.hibernate.orm.test.type; -import java.sql.Timestamp; import java.time.LocalDateTime; import java.time.Month; @@ -162,19 +161,19 @@ public class TimestampArrayTest { assertThat( tuple[1], is( new Object[] { - Timestamp.valueOf( time1 ), - Timestamp.valueOf( time2 ), - Timestamp.valueOf( time3 ) + time1, + time2, + time3 } ) ); } else { assertThat( tuple[1], - is( new Timestamp[] { - Timestamp.valueOf( time1 ), - Timestamp.valueOf( time2 ), - Timestamp.valueOf( time3 ) + is( new LocalDateTime[] { + time1, + time2, + time3 } ) ); }