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`)::
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)::

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

View File

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

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

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.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(),

View File

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

View File

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

View File

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

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