Switch back to numeric(21) with nanosecond resolution as fallback for mapping Duration to retain backwards compatibility
This commit is contained in:
parent
6801ff0f26
commit
f2aa533dfc
|
@ -383,8 +383,8 @@ This setting applies to Oracle Dialect only, and it specifies whether `byte[]` o
|
|||
`*hibernate.type.preferred_boolean_jdbc_type_code*` (e.g. `-7` for `java.sql.Types.BIT`)::
|
||||
Global setting identifying the preferred JDBC type code for storing boolean values. The fallback is to ask the Dialect.
|
||||
|
||||
`*hibernate.type.preferred_duration_jdbc_type_code*` (e.g. `2` for `java.sql.Types.NUMERIC` or `3` for `java.sql.Types.DECIMAL`)::
|
||||
Global setting identifying the preferred JDBC type code for storing duration values. The fallback is `3100` for `org.hibernate.types.SqlTypes.INTERVAL_SECOND`.
|
||||
`*hibernate.type.preferred_duration_jdbc_type_code*` (e.g. `2` for `java.sql.Types.NUMERIC` or `3100` for `org.hibernate.types.SqlTypes.INTERVAL_SECOND` (default value))::
|
||||
Global setting identifying the preferred JDBC type code for storing duration values.
|
||||
|
||||
==== Bean Validation options
|
||||
`*jakarta.persistence.validation.factory*` (e.g. `jakarta.validation.ValidationFactory` implementation)::
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.userguide.mapping.basic;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||
import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping;
|
||||
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
|
||||
import org.hibernate.persister.entity.EntityPersister;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
import org.hibernate.type.descriptor.jdbc.AdjustableJdbcType;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcType;
|
||||
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
|
||||
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.hibernate.testing.orm.junit.Setting;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@DomainModel(annotatedClasses = DurationMappingLegacyTests.EntityWithDuration.class)
|
||||
@SessionFactory
|
||||
// 2 stands for the type code Types.NUMERIC
|
||||
@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_DURATION_JDBC_TYPE_CODE, value = "2"))
|
||||
public class DurationMappingLegacyTests {
|
||||
|
||||
@Test
|
||||
public void verifyMappings(SessionFactoryScope scope) {
|
||||
final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory()
|
||||
.getRuntimeMetamodels()
|
||||
.getMappingMetamodel();
|
||||
final EntityPersister entityDescriptor = mappingMetamodel.findEntityDescriptor(EntityWithDuration.class);
|
||||
final JdbcTypeRegistry jdbcTypeRegistry = mappingMetamodel.getTypeConfiguration().getJdbcTypeRegistry();
|
||||
|
||||
final BasicAttributeMapping duration = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("duration");
|
||||
final JdbcMapping jdbcMapping = duration.getJdbcMapping();
|
||||
assertThat(jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(), equalTo(Duration.class));
|
||||
final JdbcType intervalType = jdbcTypeRegistry.getDescriptor(SqlTypes.NUMERIC);
|
||||
final JdbcType realType;
|
||||
if (intervalType instanceof AdjustableJdbcType) {
|
||||
realType = ((AdjustableJdbcType) intervalType).resolveIndicatedType(
|
||||
() -> mappingMetamodel.getTypeConfiguration(),
|
||||
jdbcMapping.getJavaTypeDescriptor()
|
||||
);
|
||||
}
|
||||
else {
|
||||
realType = intervalType;
|
||||
}
|
||||
assertThat( jdbcMapping.getJdbcType(), is( realType));
|
||||
|
||||
scope.inTransaction(
|
||||
(session) -> {
|
||||
session.persist(new EntityWithDuration(1, Duration.ofHours(3)));
|
||||
}
|
||||
);
|
||||
|
||||
scope.inTransaction(
|
||||
(session) -> session.find(EntityWithDuration.class, 1)
|
||||
);
|
||||
}
|
||||
|
||||
@Entity(name = "EntityWithDuration")
|
||||
@Table(name = "EntityWithDuration")
|
||||
public static class EntityWithDuration {
|
||||
@Id
|
||||
private Integer id;
|
||||
|
||||
//tag::basic-duration-example[]
|
||||
private Duration duration;
|
||||
//end::basic-duration-example[]
|
||||
|
||||
public EntityWithDuration() {
|
||||
}
|
||||
|
||||
public EntityWithDuration(Integer id, Duration duration) {
|
||||
this.id = id;
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ import org.hibernate.dialect.Dialect;
|
|||
import org.hibernate.engine.config.spi.ConfigurationService;
|
||||
import org.hibernate.engine.config.spi.StandardConverters;
|
||||
import org.hibernate.engine.jdbc.spi.JdbcServices;
|
||||
import org.hibernate.internal.util.config.ConfigurationHelper;
|
||||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.BasicTypeRegistry;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
@ -379,7 +380,13 @@ public class MetadataBuildingProcess {
|
|||
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.UUID, SqlTypes.BINARY );
|
||||
jdbcTypeRegistry.addDescriptorIfAbsent( JsonJdbcType.INSTANCE );
|
||||
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INET, SqlTypes.VARBINARY );
|
||||
final int preferredSqlTypeCodeForDuration = ConfigurationHelper.getPreferredSqlTypeCodeForDuration( bootstrapContext.getServiceRegistry() );
|
||||
if ( preferredSqlTypeCodeForDuration != SqlTypes.INTERVAL_SECOND ) {
|
||||
jdbcTypeRegistry.addDescriptor( SqlTypes.INTERVAL_SECOND, jdbcTypeRegistry.getDescriptor( preferredSqlTypeCodeForDuration ) );
|
||||
}
|
||||
else {
|
||||
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INTERVAL_SECOND, SqlTypes.NUMERIC );
|
||||
}
|
||||
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.GEOMETRY, SqlTypes.VARBINARY );
|
||||
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.POINT, SqlTypes.VARBINARY );
|
||||
|
||||
|
|
|
@ -1099,6 +1099,11 @@ public abstract class AbstractHANADialect extends Dialect {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFractionalSecondPrecisionInNanos() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) {
|
||||
switch (unit) {
|
||||
|
|
|
@ -271,13 +271,32 @@ public class SybaseASEDialect extends SybaseDialect {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFractionalSecondPrecisionInNanos() {
|
||||
// If the database does not support bigdatetime and bigtime types,
|
||||
// we try to operate on milliseconds instead
|
||||
if ( getVersion().isBefore( 15, 5 ) ) {
|
||||
return 1_000_000;
|
||||
}
|
||||
else {
|
||||
return 1_000;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) {
|
||||
//TODO!!
|
||||
switch ( unit ) {
|
||||
case NANOSECOND:
|
||||
case NATIVE:
|
||||
return "(datediff(mcs,?2,?3)*1000)";
|
||||
// If the database does not support bigdatetime and bigtime types,
|
||||
// we try to operate on milliseconds instead
|
||||
if ( getVersion().isBefore( 15, 5 ) ) {
|
||||
return "cast(datediff(ms,?2,?3) as numeric(21))";
|
||||
}
|
||||
else {
|
||||
return "cast(datediff(mcs,cast(?2 as bigdatetime),cast(?3 as bigdatetime)) as numeric(21))";
|
||||
}
|
||||
default:
|
||||
return "datediff(?1,?2,?3)";
|
||||
}
|
||||
|
|
|
@ -387,8 +387,8 @@ import static org.hibernate.internal.util.NullnessHelper.coalesceSuppliedValues;
|
|||
import static org.hibernate.query.sqm.BinaryArithmeticOperator.ADD;
|
||||
import static org.hibernate.query.sqm.BinaryArithmeticOperator.MULTIPLY;
|
||||
import static org.hibernate.query.sqm.BinaryArithmeticOperator.SUBTRACT;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.DAY;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.EPOCH;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.NATIVE;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.SECOND;
|
||||
import static org.hibernate.query.sqm.UnaryArithmeticOperator.UNARY_MINUS;
|
||||
import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey;
|
||||
|
@ -5438,7 +5438,8 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
|
||||
// The result of timestamp subtraction is always a `Duration`, unless a unit is applied
|
||||
// So use SECOND granularity with fractions as that is what the `DurationJavaType` expects
|
||||
final TemporalUnit baseUnit = SECOND; // todo: alternatively repurpose NATIVE to mean "INTERVAL SECOND"
|
||||
final TemporalUnit baseUnit = NATIVE;
|
||||
final BasicType<Long> diffResultType = basicType( Long.class );
|
||||
|
||||
if ( adjustedTimestamp != null ) {
|
||||
if ( appliedByUnit != null ) {
|
||||
|
@ -5451,7 +5452,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
// temporal type, so we must use it for both
|
||||
// the diff, and then the subsequent add
|
||||
|
||||
DurationUnit unit = new DurationUnit( baseUnit, basicType( Integer.class ) );
|
||||
DurationUnit unit = new DurationUnit( baseUnit, diffResultType );
|
||||
Expression magnitude = applyScale( timestampdiff().expression( null, unit, right, left ) );
|
||||
return timestampadd().expression(
|
||||
(ReturnableType<?>) adjustedTimestampType, //TODO should be adjustedTimestamp.getType()
|
||||
|
@ -5467,7 +5468,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
}
|
||||
else {
|
||||
// a plain "bare" Duration
|
||||
DurationUnit unit = new DurationUnit( baseUnit, basicType( Integer.class ) );
|
||||
DurationUnit unit = new DurationUnit( baseUnit, diffResultType );
|
||||
BasicValuedMapping durationType = (BasicValuedMapping) expression.getNodeType();
|
||||
Expression scaledMagnitude = applyScale( timestampdiff().expression(
|
||||
(ReturnableType<?>) expression.getNodeType(),
|
||||
|
|
|
@ -177,7 +177,7 @@ import org.hibernate.type.StandardBasicTypes;
|
|||
import org.hibernate.type.descriptor.WrapperOptions;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
|
||||
|
||||
import static org.hibernate.query.sqm.TemporalUnit.SECOND;
|
||||
import static org.hibernate.query.sqm.TemporalUnit.NANOSECOND;
|
||||
import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst;
|
||||
import static org.hibernate.sql.results.graph.DomainResultGraphPrinter.logDomainResultGraph;
|
||||
|
||||
|
@ -4439,8 +4439,9 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
|||
@Override
|
||||
public void visitDuration(Duration duration) {
|
||||
duration.getMagnitude().accept( this );
|
||||
// Convert to NANOSECOND because DurationJavaType requires values in that unit
|
||||
appendSql(
|
||||
duration.getUnit().conversionFactor( SECOND, getDialect() )
|
||||
duration.getUnit().conversionFactor( NANOSECOND, getDialect() )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -347,7 +347,8 @@ public final class StandardBasicTypes {
|
|||
// Date / time data
|
||||
|
||||
/**
|
||||
* The standard Hibernate type for mapping {@link Duration} to JDBC {@link org.hibernate.type.SqlTypes#NUMERIC NUMERIC}.
|
||||
* The standard Hibernate type for mapping {@link Duration} to JDBC {@link org.hibernate.type.SqlTypes#INTERVAL_SECOND INTERVAL_SECOND}
|
||||
* or {@link org.hibernate.type.SqlTypes#NUMERIC NUMERIC} as a fallback.
|
||||
*/
|
||||
public static final BasicTypeReference<Duration> DURATION = new BasicTypeReference<>(
|
||||
"Duration",
|
||||
|
|
|
@ -7,10 +7,7 @@
|
|||
package org.hibernate.type.descriptor.java;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.hibernate.dialect.Dialect;
|
||||
import org.hibernate.internal.util.StringHelper;
|
||||
|
@ -23,13 +20,13 @@ import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
|
|||
* Descriptor for {@link Duration}, which is represented internally
|
||||
* as ({@code long seconds}, {@code int nanoseconds}), approximately
|
||||
* 28 decimal digits of precision. This quantity must be stored in
|
||||
* the database as a single integer with units of nanoseconds, since
|
||||
* the ANSI SQL {@code interval} type is not well-supported.
|
||||
* the database as a single integer with units of nanoseconds, unless
|
||||
* the ANSI SQL {@code interval} type is supported.
|
||||
*
|
||||
* In practice, the 19 decimal digits of a SQL {@code bigint} are
|
||||
* capable of representing six centuries in nanoseconds and are
|
||||
* sufficient for many applications. However, by default, we map
|
||||
* Java {@link Duration} to SQL {@code numeric(21,6)} here, which
|
||||
* Java {@link Duration} to SQL {@code numeric(21)} here, which
|
||||
* can comfortably represent 60 millenia of nanos.
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
|
@ -40,13 +37,7 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
|
|||
* Singleton access
|
||||
*/
|
||||
public static final DurationJavaType INSTANCE = new DurationJavaType();
|
||||
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance( Locale.ENGLISH );
|
||||
private static final ThreadLocal<DecimalFormat> DECIMAL_FORMAT = new ThreadLocal<>() {
|
||||
@Override
|
||||
protected DecimalFormat initialValue() {
|
||||
return new DecimalFormat( "0.000000000", DECIMAL_FORMAT_SYMBOLS );
|
||||
}
|
||||
};
|
||||
private static final BigDecimal BILLION = BigDecimal.valueOf( 1_000_000_000 );
|
||||
|
||||
public DurationJavaType() {
|
||||
super( Duration.class, ImmutableMutabilityPlan.instance() );
|
||||
|
@ -54,7 +45,9 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
|
|||
|
||||
@Override
|
||||
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
|
||||
return context.getTypeConfiguration().getJdbcTypeRegistry().getDescriptor( context.getPreferredSqlTypeCodeForDuration() );
|
||||
return context.getTypeConfiguration()
|
||||
.getJdbcTypeRegistry()
|
||||
.getDescriptor( context.getPreferredSqlTypeCodeForDuration() );
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -94,8 +87,7 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
|
|||
if ( BigDecimal.class.isAssignableFrom( type ) ) {
|
||||
return (X) new BigDecimal( duration.getSeconds() )
|
||||
.movePointRight( 9 )
|
||||
.add( new BigDecimal( duration.getNano() ) )
|
||||
.movePointLeft( 9 );
|
||||
.add( new BigDecimal( duration.getNano() ) );
|
||||
}
|
||||
|
||||
if ( String.class.isAssignableFrom( type ) ) {
|
||||
|
@ -120,11 +112,17 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
|
|||
}
|
||||
|
||||
if (value instanceof BigDecimal) {
|
||||
return fromDecimal( value );
|
||||
final BigDecimal decimal = (BigDecimal) value;
|
||||
final BigDecimal[] bigDecimals = decimal.divideAndRemainder( BILLION );
|
||||
return Duration.ofSeconds(
|
||||
bigDecimals[0].longValueExact(),
|
||||
bigDecimals[1].longValueExact()
|
||||
);
|
||||
}
|
||||
|
||||
if (value instanceof Double) {
|
||||
return fromDecimal( value );
|
||||
// PostgreSQL returns a Double for datediff(epoch)
|
||||
return Duration.ofNanos( ( (Double) value ).longValue() );
|
||||
}
|
||||
|
||||
if (value instanceof Long) {
|
||||
|
@ -138,18 +136,6 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
|
|||
throw unknownWrap( value.getClass() );
|
||||
}
|
||||
|
||||
private Duration fromDecimal(Object number) {
|
||||
final String formatted = DECIMAL_FORMAT.get().format( number );
|
||||
final int dotIndex = formatted.indexOf( '.' );
|
||||
if (dotIndex == -1) {
|
||||
return Duration.ofSeconds( Long.parseLong( formatted ) );
|
||||
}
|
||||
return Duration.ofSeconds(
|
||||
Long.parseLong( formatted.substring( 0, dotIndex ) ),
|
||||
Long.parseLong( formatted.substring( dotIndex + 1 ) )
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) {
|
||||
if ( jdbcType.getDefaultSqlTypeCode() == SqlTypes.INTERVAL_SECOND ) {
|
||||
|
@ -168,6 +154,13 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
|
|||
|
||||
@Override
|
||||
public int getDefaultSqlScale(Dialect dialect, JdbcType jdbcType) {
|
||||
if ( jdbcType.getDefaultSqlTypeCode() == SqlTypes.INTERVAL_SECOND ) {
|
||||
// The default scale necessary is 9 i.e. nanosecond resolution
|
||||
return 9;
|
||||
}
|
||||
else {
|
||||
// For non-interval types, we use the type numeric(21)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,10 +256,17 @@ plural attribute classification
|
|||
See https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#collection-type-reg-ann
|
||||
for details of using `@CollectionTypeRegistration`
|
||||
|
||||
=== Misc
|
||||
=== Duration mapping changes
|
||||
|
||||
* The default type for `Duration` was changed to `NUMERIC` which could lead to schema validation errors
|
||||
Duration now maps to the type code `SqlType.INTERVAL_SECOND` by default, which maps to the SQL type `interval second`
|
||||
if possible, and falls back to `numeric(21)`.
|
||||
In either case, schema validation errors could occur as 5.x used the type code `Types.BIGINT`.
|
||||
|
||||
Migration to `numeric(21)` should be easy. The migration to `interval second` might require a migration expression like
|
||||
`cast(cast(old as numeric(21,9) / 1000000000) as interval second(9))`.
|
||||
|
||||
To retain backwards compatibility, configure the setting `hibernate.type.preferred_duration_jdbc_type_code` to `2`
|
||||
which stands for `Types.NUMERIC`.
|
||||
|
||||
[[query]]
|
||||
== Query
|
||||
|
|
Loading…
Reference in New Issue