Switch back to numeric(21) with nanosecond resolution as fallback for mapping Duration to retain backwards compatibility

This commit is contained in:
Christian Beikov 2022-03-22 18:27:31 +01:00
parent 6801ff0f26
commit f2aa533dfc
10 changed files with 176 additions and 44 deletions

View File

@ -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`):: `*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. 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`):: `*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. The fallback is `3100` for `org.hibernate.types.SqlTypes.INTERVAL_SECOND`. Global setting identifying the preferred JDBC type code for storing duration values.
==== Bean Validation options ==== Bean Validation options
`*jakarta.persistence.validation.factory*` (e.g. `jakarta.validation.ValidationFactory` implementation):: `*jakarta.persistence.validation.factory*` (e.g. `jakarta.validation.ValidationFactory` implementation)::

View File

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

View File

@ -42,6 +42,7 @@ import org.hibernate.dialect.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.config.spi.StandardConverters;
import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.type.BasicType; import org.hibernate.type.BasicType;
import org.hibernate.type.BasicTypeRegistry; import org.hibernate.type.BasicTypeRegistry;
import org.hibernate.type.SqlTypes; import org.hibernate.type.SqlTypes;
@ -379,7 +380,13 @@ public class MetadataBuildingProcess {
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.UUID, SqlTypes.BINARY ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.UUID, SqlTypes.BINARY );
jdbcTypeRegistry.addDescriptorIfAbsent( JsonJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( JsonJdbcType.INSTANCE );
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INET, SqlTypes.VARBINARY ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INET, SqlTypes.VARBINARY );
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INTERVAL_SECOND, SqlTypes.NUMERIC ); 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.GEOMETRY, SqlTypes.VARBINARY );
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.POINT, SqlTypes.VARBINARY ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.POINT, SqlTypes.VARBINARY );

View File

@ -1099,6 +1099,11 @@ public abstract class AbstractHANADialect extends Dialect {
return false; return false;
} }
@Override
public long getFractionalSecondPrecisionInNanos() {
return 100;
}
@Override @Override
public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) {
switch (unit) { switch (unit) {

View File

@ -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 @Override
public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) {
//TODO!! //TODO!!
switch ( unit ) { switch ( unit ) {
case NANOSECOND: case NANOSECOND:
case NATIVE: 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: default:
return "datediff(?1,?2,?3)"; return "datediff(?1,?2,?3)";
} }

View File

@ -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.ADD;
import static org.hibernate.query.sqm.BinaryArithmeticOperator.MULTIPLY; import static org.hibernate.query.sqm.BinaryArithmeticOperator.MULTIPLY;
import static org.hibernate.query.sqm.BinaryArithmeticOperator.SUBTRACT; 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.EPOCH;
import static org.hibernate.query.sqm.TemporalUnit.NATIVE;
import static org.hibernate.query.sqm.TemporalUnit.SECOND; import static org.hibernate.query.sqm.TemporalUnit.SECOND;
import static org.hibernate.query.sqm.UnaryArithmeticOperator.UNARY_MINUS; import static org.hibernate.query.sqm.UnaryArithmeticOperator.UNARY_MINUS;
import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; 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 // 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 // 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 ( adjustedTimestamp != null ) {
if ( appliedByUnit != 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 // temporal type, so we must use it for both
// the diff, and then the subsequent add // 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 ) ); Expression magnitude = applyScale( timestampdiff().expression( null, unit, right, left ) );
return timestampadd().expression( return timestampadd().expression(
(ReturnableType<?>) adjustedTimestampType, //TODO should be adjustedTimestamp.getType() (ReturnableType<?>) adjustedTimestampType, //TODO should be adjustedTimestamp.getType()
@ -5467,7 +5468,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
} }
else { else {
// a plain "bare" Duration // a plain "bare" Duration
DurationUnit unit = new DurationUnit( baseUnit, basicType( Integer.class ) ); DurationUnit unit = new DurationUnit( baseUnit, diffResultType );
BasicValuedMapping durationType = (BasicValuedMapping) expression.getNodeType(); BasicValuedMapping durationType = (BasicValuedMapping) expression.getNodeType();
Expression scaledMagnitude = applyScale( timestampdiff().expression( Expression scaledMagnitude = applyScale( timestampdiff().expression(
(ReturnableType<?>) expression.getNodeType(), (ReturnableType<?>) expression.getNodeType(),

View File

@ -177,7 +177,7 @@ import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; 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.ast.SqlTreePrinter.logSqlAst;
import static org.hibernate.sql.results.graph.DomainResultGraphPrinter.logDomainResultGraph; import static org.hibernate.sql.results.graph.DomainResultGraphPrinter.logDomainResultGraph;
@ -4439,8 +4439,9 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
@Override @Override
public void visitDuration(Duration duration) { public void visitDuration(Duration duration) {
duration.getMagnitude().accept( this ); duration.getMagnitude().accept( this );
// Convert to NANOSECOND because DurationJavaType requires values in that unit
appendSql( appendSql(
duration.getUnit().conversionFactor( SECOND, getDialect() ) duration.getUnit().conversionFactor( NANOSECOND, getDialect() )
); );
} }

View File

@ -347,7 +347,8 @@ public final class StandardBasicTypes {
// Date / time data // 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<>( public static final BasicTypeReference<Duration> DURATION = new BasicTypeReference<>(
"Duration", "Duration",

View File

@ -7,10 +7,7 @@
package org.hibernate.type.descriptor.java; package org.hibernate.type.descriptor.java;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.Duration; import java.time.Duration;
import java.util.Locale;
import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Dialect;
import org.hibernate.internal.util.StringHelper; 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 * Descriptor for {@link Duration}, which is represented internally
* as ({@code long seconds}, {@code int nanoseconds}), approximately * as ({@code long seconds}, {@code int nanoseconds}), approximately
* 28 decimal digits of precision. This quantity must be stored in * 28 decimal digits of precision. This quantity must be stored in
* the database as a single integer with units of nanoseconds, since * the database as a single integer with units of nanoseconds, unless
* the ANSI SQL {@code interval} type is not well-supported. * the ANSI SQL {@code interval} type is supported.
* *
* In practice, the 19 decimal digits of a SQL {@code bigint} are * In practice, the 19 decimal digits of a SQL {@code bigint} are
* capable of representing six centuries in nanoseconds and are * capable of representing six centuries in nanoseconds and are
* sufficient for many applications. However, by default, we map * 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. * can comfortably represent 60 millenia of nanos.
* *
* @author Steve Ebersole * @author Steve Ebersole
@ -40,13 +37,7 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
* Singleton access * Singleton access
*/ */
public static final DurationJavaType INSTANCE = new DurationJavaType(); public static final DurationJavaType INSTANCE = new DurationJavaType();
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance( Locale.ENGLISH ); private static final BigDecimal BILLION = BigDecimal.valueOf( 1_000_000_000 );
private static final ThreadLocal<DecimalFormat> DECIMAL_FORMAT = new ThreadLocal<>() {
@Override
protected DecimalFormat initialValue() {
return new DecimalFormat( "0.000000000", DECIMAL_FORMAT_SYMBOLS );
}
};
public DurationJavaType() { public DurationJavaType() {
super( Duration.class, ImmutableMutabilityPlan.instance() ); super( Duration.class, ImmutableMutabilityPlan.instance() );
@ -54,7 +45,9 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
@Override @Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
return context.getTypeConfiguration().getJdbcTypeRegistry().getDescriptor( context.getPreferredSqlTypeCodeForDuration() ); return context.getTypeConfiguration()
.getJdbcTypeRegistry()
.getDescriptor( context.getPreferredSqlTypeCodeForDuration() );
} }
@Override @Override
@ -94,8 +87,7 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
if ( BigDecimal.class.isAssignableFrom( type ) ) { if ( BigDecimal.class.isAssignableFrom( type ) ) {
return (X) new BigDecimal( duration.getSeconds() ) return (X) new BigDecimal( duration.getSeconds() )
.movePointRight( 9 ) .movePointRight( 9 )
.add( new BigDecimal( duration.getNano() ) ) .add( new BigDecimal( duration.getNano() ) );
.movePointLeft( 9 );
} }
if ( String.class.isAssignableFrom( type ) ) { if ( String.class.isAssignableFrom( type ) ) {
@ -120,11 +112,17 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
} }
if (value instanceof BigDecimal) { 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) { if (value instanceof Double) {
return fromDecimal( value ); // PostgreSQL returns a Double for datediff(epoch)
return Duration.ofNanos( ( (Double) value ).longValue() );
} }
if (value instanceof Long) { if (value instanceof Long) {
@ -138,18 +136,6 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
throw unknownWrap( value.getClass() ); 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 @Override
public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) { public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) {
if ( jdbcType.getDefaultSqlTypeCode() == SqlTypes.INTERVAL_SECOND ) { if ( jdbcType.getDefaultSqlTypeCode() == SqlTypes.INTERVAL_SECOND ) {
@ -168,6 +154,13 @@ public class DurationJavaType extends AbstractClassJavaType<Duration> {
@Override @Override
public int getDefaultSqlScale(Dialect dialect, JdbcType jdbcType) { public int getDefaultSqlScale(Dialect dialect, JdbcType jdbcType) {
return 9; 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;
}
} }
} }

View File

@ -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 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` 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]]
== Query == Query