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 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

View File

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

View File

@ -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();
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <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
* 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.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

View File

@ -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.
*
* <p>
* 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();

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -379,12 +379,11 @@ public class NativeQueryResultTypeAutoDiscoveryTest {
private Map<Object, Object> buildSettings(Class<?> ... entityTypes) {
Map<Object, Object> 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;
}

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;
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 } )
);
}
} );

View File

@ -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 } )
);
}
} );

View File

@ -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
} )
);
}