HHH-17557 native queries return LocalDate and LocalDateTime instead of java.sql types

... by default, with a setting to recover old behavior.
This commit is contained in:
Gavin King 2024-09-14 22:04:19 +02:00
parent 2fc51bd7b2
commit 2e6902ddb2
14 changed files with 165 additions and 67 deletions

View File

@ -209,6 +209,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private boolean collectionsInDefaultFetchGroupEnabled = true; private boolean collectionsInDefaultFetchGroupEnabled = true;
private final boolean UnownedAssociationTransientCheck; private final boolean UnownedAssociationTransientCheck;
private final boolean passProcedureParameterNames; private final boolean passProcedureParameterNames;
private final boolean preferJdbcDatetimeTypes;
// JPA callbacks // JPA callbacks
private final boolean callbacksEnabled; private final boolean callbacksEnabled;
@ -635,6 +636,12 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
configurationSettings, configurationSettings,
false false
); );
this.preferJdbcDatetimeTypes = ConfigurationHelper.getBoolean(
AvailableSettings.NATIVE_PREFER_JDBC_DATETIME_TYPES,
configurationSettings,
false
);
} }
private boolean disallowBatchUpdates(Dialect dialect, ExtractedDatabaseMetaData meta) { private boolean disallowBatchUpdates(Dialect dialect, ExtractedDatabaseMetaData meta) {
@ -1326,6 +1333,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
return passProcedureParameterNames; return passProcedureParameterNames;
} }
@Override
public boolean isPreferJdbcDatetimeTypesInNativeQueriesEnabled() {
return preferJdbcDatetimeTypes;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// In-flight mutation access // In-flight mutation access

View File

@ -512,4 +512,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
public boolean isPassProcedureParameterNames() { public boolean isPassProcedureParameterNames() {
return delegate.isPassProcedureParameterNames(); return delegate.isPassProcedureParameterNames();
} }
@Override
public boolean isPreferJdbcDatetimeTypesInNativeQueriesEnabled() {
return delegate.isPreferJdbcDatetimeTypesInNativeQueriesEnabled();
}
} }

View File

@ -365,4 +365,14 @@ public interface SessionFactoryOptions extends QueryEngineOptions {
} }
boolean isPassProcedureParameterNames(); 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();
} }

View File

@ -25,6 +25,7 @@ public interface QuerySettings {
* @since 6.5 * @since 6.5
*/ */
String PORTABLE_INTEGER_DIVISION = "hibernate.query.hql.portable_integer_division"; String PORTABLE_INTEGER_DIVISION = "hibernate.query.hql.portable_integer_division";
/** /**
* Specifies a {@link org.hibernate.query.hql.HqlTranslator} to use for HQL query * Specifies a {@link org.hibernate.query.hql.HqlTranslator} to use for HQL query
* translation. * translation.
@ -135,15 +136,28 @@ public interface QuerySettings {
String CRITERIA_COPY_TREE = "hibernate.criteria.copy_tree"; 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. * When enabled, ordinal parameters (represented by the {@code ?} placeholder) in
* <p> * native queries will be ignored.
* By default, this is set to false, i.e. native queries will be checked for ordinal placeholders.
* <p> * <p>
* By default, native queries are checked for ordinal placeholders.
* *
* @see SessionFactoryOptions#getNativeJdbcParametersIgnored() * @see SessionFactoryOptions#getNativeJdbcParametersIgnored()
*/ */
String NATIVE_IGNORE_JDBC_PARAMETERS = "hibernate.query.native.ignore_jdbc_parameters"; 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.
* <p>
* 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 * 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 * in combination with a {@code fetch join} applied to a collection or many-valued

View File

@ -12,6 +12,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.time.LocalDate;
import java.util.Calendar; import java.util.Calendar;
import jakarta.persistence.TemporalType; import jakarta.persistence.TemporalType;
@ -54,7 +55,9 @@ public class DateJdbcType implements JdbcType {
Integer length, Integer length,
Integer scale, Integer scale,
TypeConfiguration typeConfiguration) { TypeConfiguration typeConfiguration) {
return typeConfiguration.getJavaTypeRegistry().getDescriptor( Date.class ); return typeConfiguration.getCurrentBaseSqlTypeIndicators().preferJdbcDatetimeTypes()
? typeConfiguration.getJavaTypeRegistry().getDescriptor( Date.class )
: typeConfiguration.getJavaTypeRegistry().getDescriptor( LocalDate.class );
} }
@Override @Override

View File

@ -9,7 +9,6 @@ package org.hibernate.type.descriptor.jdbc;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.TemporalType; import jakarta.persistence.TemporalType;
import org.hibernate.AssertionFailure;
import org.hibernate.Incubating; import org.hibernate.Incubating;
import org.hibernate.TimeZoneStorageStrategy; import org.hibernate.TimeZoneStorageStrategy;
import org.hibernate.dialect.Dialect; 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. * Resolves the given type code to a possibly different type code, based on context.
* * <p>
* A database might not support a certain type code in certain scenarios like within a UDT * 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. * and has to resolve to a different type code in such a scenario.
* *
@ -210,6 +209,18 @@ public interface JdbcTypeIndicators {
return jdbcTypeCode; 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. * 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 * @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_WITH_TIMEZONE
* @see SqlTypes#TIME * @see SqlTypes#TIME
* @see SqlTypes#TIME_UTC * @see SqlTypes#TIME_UTC
*/ */
static int getZonedTimeSqlType(TimeZoneStorageStrategy storageStrategy) { static int getZonedTimeSqlType(TimeZoneStorageStrategy storageStrategy) {
switch ( storageStrategy ) { return switch (storageStrategy) {
case NATIVE: case NATIVE -> SqlTypes.TIME_WITH_TIMEZONE;
return SqlTypes.TIME_WITH_TIMEZONE; case COLUMN, NORMALIZE -> SqlTypes.TIME;
case COLUMN: case NORMALIZE_UTC -> SqlTypes.TIME_UTC;
case NORMALIZE: };
return SqlTypes.TIME;
case NORMALIZE_UTC:
return SqlTypes.TIME_UTC;
default:
throw new AssertionFailure( "unknown time zone storage strategy" );
}
} }
/** /**
* @return the SQL column type used for storing datetimes under the * @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#TIME_WITH_TIMEZONE
* @see SqlTypes#TIMESTAMP * @see SqlTypes#TIMESTAMP
* @see SqlTypes#TIMESTAMP_UTC * @see SqlTypes#TIMESTAMP_UTC
*/ */
static int getZonedTimestampSqlType(TimeZoneStorageStrategy storageStrategy) { static int getZonedTimestampSqlType(TimeZoneStorageStrategy storageStrategy) {
switch ( storageStrategy ) { return switch (storageStrategy) {
case NATIVE: case NATIVE -> SqlTypes.TIMESTAMP_WITH_TIMEZONE;
return SqlTypes.TIMESTAMP_WITH_TIMEZONE; case COLUMN, NORMALIZE -> SqlTypes.TIMESTAMP;
case COLUMN: case NORMALIZE_UTC ->
case NORMALIZE:
return SqlTypes.TIMESTAMP;
case NORMALIZE_UTC:
// sensitive to hibernate.type.preferred_instant_jdbc_type // sensitive to hibernate.type.preferred_instant_jdbc_type
return SqlTypes.TIMESTAMP_UTC; SqlTypes.TIMESTAMP_UTC;
default: };
throw new AssertionFailure( "unknown time zone storage strategy" );
}
} }
/** /**
@ -286,17 +286,13 @@ public interface JdbcTypeIndicators {
*/ */
default int getDefaultZonedTimestampSqlType() { default int getDefaultZonedTimestampSqlType() {
final TemporalType temporalPrecision = getTemporalPrecision(); final TemporalType temporalPrecision = getTemporalPrecision();
switch ( temporalPrecision == null ? TemporalType.TIMESTAMP : temporalPrecision ) { return switch (temporalPrecision == null ? TemporalType.TIMESTAMP : temporalPrecision) {
case TIME: case TIME -> getZonedTimeSqlType( getDefaultTimeZoneStorageStrategy() );
return getZonedTimeSqlType( getDefaultTimeZoneStorageStrategy() ); case DATE -> Types.DATE;
case DATE: case TIMESTAMP ->
return Types.DATE;
case TIMESTAMP:
// sensitive to hibernate.timezone.default_storage // sensitive to hibernate.timezone.default_storage
return getZonedTimestampSqlType( getDefaultTimeZoneStorageStrategy() ); getZonedTimestampSqlType( getDefaultTimeZoneStorageStrategy() );
default: };
throw new IllegalArgumentException( "Unexpected jakarta.persistence.TemporalType : " + temporalPrecision);
}
} }
Dialect getDialect(); Dialect getDialect();

View File

@ -12,6 +12,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Time; import java.sql.Time;
import java.sql.Types; import java.sql.Types;
import java.time.LocalTime;
import java.util.Calendar; import java.util.Calendar;
import jakarta.persistence.TemporalType; import jakarta.persistence.TemporalType;
@ -54,7 +55,9 @@ public class TimeJdbcType implements JdbcType {
Integer length, Integer length,
Integer scale, Integer scale,
TypeConfiguration typeConfiguration) { TypeConfiguration typeConfiguration) {
return typeConfiguration.getJavaTypeRegistry().getDescriptor( Time.class ); return typeConfiguration.getCurrentBaseSqlTypeIndicators().preferJdbcDatetimeTypes()
? typeConfiguration.getJavaTypeRegistry().getDescriptor( Time.class )
: typeConfiguration.getJavaTypeRegistry().getDescriptor( LocalTime.class );
} }
@Override @Override

View File

@ -12,6 +12,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.sql.Types; import java.sql.Types;
import java.time.LocalDateTime;
import java.util.Calendar; import java.util.Calendar;
import jakarta.persistence.TemporalType; import jakarta.persistence.TemporalType;
@ -54,7 +55,9 @@ public class TimestampJdbcType implements JdbcType {
Integer length, Integer length,
Integer scale, Integer scale,
TypeConfiguration typeConfiguration) { TypeConfiguration typeConfiguration) {
return typeConfiguration.getJavaTypeRegistry().getDescriptor( Timestamp.class ); return typeConfiguration.getCurrentBaseSqlTypeIndicators().preferJdbcDatetimeTypes()
? typeConfiguration.getJavaTypeRegistry().getDescriptor( Timestamp.class )
: typeConfiguration.getJavaTypeRegistry().getDescriptor( LocalDateTime.class );
} }
@Override @Override

View File

@ -482,6 +482,12 @@ public class TypeConfiguration implements SessionFactoryObserver, Serializable {
: sessionFactory.getJdbcServices().getDialect(); : sessionFactory.getJdbcServices().getDialect();
} }
@Override
public boolean preferJdbcDatetimeTypes() {
return sessionFactory != null
&& sessionFactory.getSessionFactoryOptions().isPreferJdbcDatetimeTypesInNativeQueriesEnabled();
}
private Scope(TypeConfiguration typeConfiguration) { private Scope(TypeConfiguration typeConfiguration) {
this.typeConfiguration = typeConfiguration; this.typeConfiguration = typeConfiguration;
} }

View File

@ -379,12 +379,11 @@ public class NativeQueryResultTypeAutoDiscoveryTest {
private Map<Object, Object> buildSettings(Class<?> ... entityTypes) { private Map<Object, Object> buildSettings(Class<?> ... entityTypes) {
Map<Object, Object> settings = new HashMap<>(); Map<Object, Object> settings = new HashMap<>();
settings.put( AvailableSettings.NATIVE_PREFER_JDBC_DATETIME_TYPES, "true" );
settings.put( org.hibernate.cfg.AvailableSettings.HBM2DDL_AUTO, "create-drop" ); settings.put( AvailableSettings.HBM2DDL_AUTO, "create-drop" );
settings.put( org.hibernate.cfg.AvailableSettings.DIALECT, DIALECT.getClass().getName() ); settings.put( AvailableSettings.DIALECT, DIALECT.getClass().getName() );
settings.put( AvailableSettings.LOADED_CLASSES, Arrays.asList( entityTypes ) ); settings.put( AvailableSettings.LOADED_CLASSES, Arrays.asList( entityTypes ) );
ServiceRegistryUtil.applySettings( settings ); ServiceRegistryUtil.applySettings( settings );
return settings; return settings;
} }

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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();
}
}

View File

@ -6,8 +6,6 @@
*/ */
package org.hibernate.orm.test.type; package org.hibernate.orm.test.type;
import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDate; import java.time.LocalDate;
import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Dialect;
@ -39,7 +37,6 @@ import jakarta.persistence.Table;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.core.Is.is; import static org.hamcrest.core.Is.is;
/** /**
@ -164,16 +161,16 @@ public class DateArrayTest {
assertThat( assertThat(
tuple[1], tuple[1],
is( new Object[] { is( new Object[] {
new Timestamp( Date.valueOf( date1 ).getTime() ), date1,
new Timestamp( Date.valueOf( date2 ).getTime() ), date2,
new Timestamp( Date.valueOf( date3 ).getTime() ) date3
} ) } )
); );
} }
else { else {
assertThat( assertThat(
tuple[1], tuple[1],
is( new Date[] { Date.valueOf( date1 ), Date.valueOf( date2 ), Date.valueOf( date3 ) } ) is( new LocalDate[] { date1, date2, date3 } )
); );
} }
} ); } );

View File

@ -6,8 +6,6 @@
*/ */
package org.hibernate.orm.test.type; package org.hibernate.orm.test.type;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalTime; import java.time.LocalTime;
import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Dialect;
@ -158,16 +156,16 @@ public class TimeArrayTest {
assertThat( assertThat(
tuple[1], tuple[1],
is( new Object[] { is( new Object[] {
new Timestamp( Time.valueOf( time1 ).getTime() ), time1,
new Timestamp( Time.valueOf( time2 ).getTime() ), time2,
new Timestamp( Time.valueOf( time3 ).getTime() ) time3
} ) } )
); );
} }
else { else {
assertThat( assertThat(
tuple[1], tuple[1],
is( new Time[] { Time.valueOf( time1 ), Time.valueOf( time2 ), Time.valueOf( time3 ) } ) is( new LocalTime[] { time1, time2, time3 } )
); );
} }
} ); } );

View File

@ -6,7 +6,6 @@
*/ */
package org.hibernate.orm.test.type; package org.hibernate.orm.test.type;
import java.sql.Timestamp;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Month; import java.time.Month;
@ -162,19 +161,19 @@ public class TimestampArrayTest {
assertThat( assertThat(
tuple[1], tuple[1],
is( new Object[] { is( new Object[] {
Timestamp.valueOf( time1 ), time1,
Timestamp.valueOf( time2 ), time2,
Timestamp.valueOf( time3 ) time3
} ) } )
); );
} }
else { else {
assertThat( assertThat(
tuple[1], tuple[1],
is( new Timestamp[] { is( new LocalDateTime[] {
Timestamp.valueOf( time1 ), time1,
Timestamp.valueOf( time2 ), time2,
Timestamp.valueOf( time3 ) time3
} ) } )
); );
} }