HHH-14672 - Allow specifying CHAR-based storage for UUID mappings

This commit is contained in:
Steve Ebersole 2022-03-25 20:06:48 -05:00 committed by Christian Beikov
parent 21a343fc60
commit 814c164c81
18 changed files with 225 additions and 21 deletions

View File

@ -380,11 +380,15 @@ Unless specified, the JDBC Driver uses the default JVM time zone. If a different
`*hibernate.dialect.oracle.prefer_long_raw*` (e.g. `true` or `false` (default value))::
This setting applies to Oracle Dialect only, and it specifies whether `byte[]` or `Byte[]` arrays should be mapped to the deprecated `LONG RAW` (when this configuration property value is `true`) or to a `BLOB` column type (when this configuration property value is `false`).
`*hibernate.type.preferred_boolean_jdbc_type_code*` (e.g. `-7` for `java.sql.Types.BIT`)::
`*hibernate.type.preferred_boolean_jdbc_type*` (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.
Can also specify the name of the constant in `org.hibernate.type.SqlTypes` instead.
`*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))::
`*hibernate.type.preferred_uuid_jdbc_type*` (e.g. `1` for `java.sql.Types.CHAR` or `3000` for `org.hibernate.types.SqlTypes.UUID` (default value))::
Global setting identifying the preferred JDBC type code for storing uuid values.
Can also specify the name of the constant in `org.hibernate.type.SqlTypes` instead.
`*hibernate.type.preferred_duration_jdbc_type*` (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.
Can also specify the name of the constant in `org.hibernate.type.SqlTypes` instead.

View File

@ -38,7 +38,7 @@
*/
@DomainModel(annotatedClasses = DurationMappingLegacyTests.EntityWithDuration.class)
@SessionFactory
@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_DURATION_JDBC_TYPE_CODE, value = "NUMERIC"))
@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_DURATION_JDBC_TYPE, value = "NUMERIC"))
public class DurationMappingLegacyTests {
@Test

View File

@ -213,6 +213,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private boolean namedQueryStartupCheckingEnabled;
private final int preferredSqlTypeCodeForBoolean;
private final int preferredSqlTypeCodeForDuration;
private final int preferredSqlTypeCodeForUuid;
private final TimeZoneStorageStrategy defaultTimeZoneStorageStrategy;
// Caching
@ -417,6 +418,7 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo
this.namedQueryStartupCheckingEnabled = cfgService.getSetting( QUERY_STARTUP_CHECKING, BOOLEAN, true );
this.preferredSqlTypeCodeForBoolean = ConfigurationHelper.getPreferredSqlTypeCodeForBoolean( serviceRegistry );
this.preferredSqlTypeCodeForDuration = ConfigurationHelper.getPreferredSqlTypeCodeForDuration( serviceRegistry );
this.preferredSqlTypeCodeForUuid = ConfigurationHelper.getPreferredSqlTypeCodeForUuid( serviceRegistry );
this.defaultTimeZoneStorageStrategy = context.getMetadataBuildingOptions().getDefaultTimeZoneStorage();
final RegionFactory regionFactory = serviceRegistry.getService( RegionFactory.class );
@ -1195,6 +1197,11 @@ public int getPreferredSqlTypeCodeForDuration() {
return preferredSqlTypeCodeForDuration;
}
@Override
public int getPreferredSqlTypeCodeForUuid() {
return preferredSqlTypeCodeForUuid;
}
@Override
public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() {
return defaultTimeZoneStorageStrategy;

View File

@ -377,7 +377,13 @@ private static void handleTypes(BootstrapContext bootstrapContext, MetadataBuild
// add fallback type descriptors
final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry();
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.UUID, SqlTypes.BINARY );
final int preferredSqlTypeCodeForUuid = ConfigurationHelper.getPreferredSqlTypeCodeForUuid( bootstrapContext.getServiceRegistry() );
if ( preferredSqlTypeCodeForUuid != SqlTypes.UUID ) {
jdbcTypeRegistry.addDescriptor( SqlTypes.UUID, jdbcTypeRegistry.getDescriptor( preferredSqlTypeCodeForUuid ) );
}
else {
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.UUID, SqlTypes.BINARY );
}
jdbcTypeRegistry.addDescriptorIfAbsent( JsonJdbcType.INSTANCE );
addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INET, SqlTypes.VARBINARY );
final int preferredSqlTypeCodeForDuration = ConfigurationHelper.getPreferredSqlTypeCodeForDuration( bootstrapContext.getServiceRegistry() );

View File

@ -442,6 +442,11 @@ public int getPreferredSqlTypeCodeForDuration() {
return delegate.getPreferredSqlTypeCodeForDuration();
}
@Override
public int getPreferredSqlTypeCodeForUuid() {
return delegate.getPreferredSqlTypeCodeForUuid();
}
@Override
public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() {
return delegate.getDefaultTimeZoneStorageStrategy();

View File

@ -59,6 +59,10 @@ default int getPreferredSqlTypeCodeForDuration() {
return ConfigurationHelper.getPreferredSqlTypeCodeForDuration( getBootstrapContext().getServiceRegistry() );
}
default int getPreferredSqlTypeCodeForUuid() {
return ConfigurationHelper.getPreferredSqlTypeCodeForUuid( getBootstrapContext().getServiceRegistry() );
}
TypeDefinitionRegistry getTypeDefinitionRegistry();
/**

View File

@ -302,6 +302,8 @@ default boolean isCollectionsInDefaultFetchGroupEnabled() {
int getPreferredSqlTypeCodeForDuration();
int getPreferredSqlTypeCodeForUuid();
TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy();
FormatMapper getJsonFormatMapper();

View File

@ -2474,26 +2474,45 @@ public interface AvailableSettings {
String SEQUENCE_INCREMENT_SIZE_MISMATCH_STRATEGY = "hibernate.id.sequence.increment_size_mismatch_strategy";
/**
* Specifies the preferred JDBC type code for storing boolean values. When no
* type code is explicitly specified, a sensible
* Specifies the preferred JDBC type for storing boolean values. When no
* type is explicitly specified, a sensible
* {@link org.hibernate.dialect.Dialect#getPreferredSqlTypeCodeForBoolean()
* dialect-specific default type code} is used.
*
* Can also specify the name of the constant in {@link org.hibernate.type.SqlTypes} instead.
* Can be overridden locally using {@link org.hibernate.annotations.JdbcType},
* {@link org.hibernate.annotations.JdbcTypeCode} and friends
*
* Can also specify the name of the constant in {@link org.hibernate.type.SqlTypes} instead. E.g.
* {@code hibernate.type.preferred_boolean_jdbc_type=BIT}
*
* @since 6.0
*/
String PREFERRED_BOOLEAN_JDBC_TYPE_CODE = "hibernate.type.preferred_boolean_jdbc_type_code";
String PREFERRED_BOOLEAN_JDBC_TYPE = "hibernate.type.preferred_boolean_jdbc_type";
/**
* Specifies the preferred JDBC type code for storing duration values. When no
* type code is explicitly specified, {@link org.hibernate.type.SqlTypes#INTERVAL_SECOND} is used.
* The preferred JDBC type to use for storing {@link java.util.UUID} values.
*
* Can also specify the name of the constant in {@link org.hibernate.type.SqlTypes} instead.
* Can be overridden locally using {@link org.hibernate.annotations.JdbcType},
* {@link org.hibernate.annotations.JdbcTypeCode} and friends
*
* Can also specify the name of the constant in {@link org.hibernate.type.SqlTypes} instead. E.g.
* {@code hibernate.type.preferred_uuid_jdbc_type=CHAR}
*/
String PREFERRED_UUID_JDBC_TYPE = "hibernate.type.preferred_uuid_jdbc_type";
/**
* Specifies the preferred JDBC type for storing duration values. When no
* type is explicitly specified, {@link org.hibernate.type.SqlTypes#INTERVAL_SECOND} is used.
*
* Can be overridden locally using {@link org.hibernate.annotations.JdbcType},
* {@link org.hibernate.annotations.JdbcTypeCode} and friends
*
* Can also specify the name of the constant in {@link org.hibernate.type.SqlTypes} instead. E.g.
* {@code hibernate.type.preferred_duration_jdbc_type=NUMERIC}
*
* @since 6.0
*/
String PREFERRED_DURATION_JDBC_TYPE_CODE = "hibernate.type.preferred_duration_jdbc_type_code";
String PREFERRED_DURATION_JDBC_TYPE = "hibernate.type.preferred_duration_jdbc_type";
/**
* Specifies a {@link org.hibernate.type.FormatMapper} used for for JSON serialization

View File

@ -221,6 +221,11 @@ public int getPreferredSqlTypeCodeForDuration() {
return buildingContext.getPreferredSqlTypeCodeForDuration();
}
@Override
public int getPreferredSqlTypeCodeForUuid() {
return buildingContext.getPreferredSqlTypeCodeForUuid();
}
@Override
public boolean isNationalized() {
return isNationalized;

View File

@ -16,7 +16,6 @@
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.cfg.AvailableSettings;
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.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
@ -516,7 +515,7 @@ private static String extractFromSystem(String systemPropertyName) {
public static synchronized int getPreferredSqlTypeCodeForBoolean(StandardServiceRegistry serviceRegistry) {
final Integer typeCode = serviceRegistry.getService( ConfigurationService.class ).getSetting(
AvailableSettings.PREFERRED_BOOLEAN_JDBC_TYPE_CODE,
AvailableSettings.PREFERRED_BOOLEAN_JDBC_TYPE,
TypeCodeConverter.INSTANCE
);
if ( typeCode != null ) {
@ -532,12 +531,20 @@ public static synchronized int getPreferredSqlTypeCodeForBoolean(StandardService
public static synchronized int getPreferredSqlTypeCodeForDuration(StandardServiceRegistry serviceRegistry) {
return serviceRegistry.getService( ConfigurationService.class ).getSetting(
AvailableSettings.PREFERRED_DURATION_JDBC_TYPE_CODE,
AvailableSettings.PREFERRED_DURATION_JDBC_TYPE,
TypeCodeConverter.INSTANCE,
SqlTypes.INTERVAL_SECOND
);
}
public static synchronized int getPreferredSqlTypeCodeForUuid(StandardServiceRegistry serviceRegistry) {
return serviceRegistry.getService( ConfigurationService.class ).getSetting(
AvailableSettings.PREFERRED_UUID_JDBC_TYPE,
TypeCodeConverter.INSTANCE,
SqlTypes.UUID
);
}
private static class TypeCodeConverter implements ConfigurationService.Converter<Integer> {
public static final TypeCodeConverter INSTANCE = new TypeCodeConverter();

View File

@ -684,6 +684,11 @@ public int getPreferredSqlTypeCodeForDuration() {
return getBuildingContext().getPreferredSqlTypeCodeForDuration();
}
@Override
public int getPreferredSqlTypeCodeForUuid() {
return getBuildingContext().getPreferredSqlTypeCodeForUuid();
}
@Override
public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() {
if ( timeZoneStorageType != null ) {

View File

@ -598,6 +598,11 @@ public int getPreferredSqlTypeCodeForDuration() {
return creationContext.getSessionFactory().getSessionFactoryOptions().getPreferredSqlTypeCodeForDuration();
}
@Override
public int getPreferredSqlTypeCodeForUuid() {
return creationContext.getSessionFactory().getSessionFactoryOptions().getPreferredSqlTypeCodeForUuid();
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// FromClauseAccess

View File

@ -11,7 +11,6 @@
import org.hibernate.dialect.Dialect;
import org.hibernate.internal.util.BytesHelper;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
@ -30,7 +29,7 @@ public UUIDJavaType() {
@Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
return context.getTypeConfiguration().getJdbcTypeRegistry().getDescriptor( SqlTypes.UUID );
return context.getTypeConfiguration().getJdbcTypeRegistry().getDescriptor( context.getPreferredSqlTypeCodeForUuid() );
}
public String toString(UUID value) {

View File

@ -83,6 +83,16 @@ default int getPreferredSqlTypeCodeForDuration() {
return SqlTypes.INTERVAL_SECOND;
}
/**
* When mapping a uuid type to the database what is the preferred SQL type code to use?
* <p/>
* Specifically names the key into the
* {@link JdbcTypeRegistry}.
*/
default int getPreferredSqlTypeCodeForUuid() {
return SqlTypes.UUID;
}
/**
* Useful for resolutions based on column length. E.g. choosing between a VARCHAR (String) and a CHAR(1) (Character/char)
*/

View File

@ -12,7 +12,6 @@
import java.math.BigInteger;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;

View File

@ -35,10 +35,10 @@
/**
* @author Steve Ebersole
*/
@DomainModel(annotatedClasses = { UUIDCharTest.Node.class })
@DomainModel(annotatedClasses = { UuidAsCharAnnotationTest.Node.class })
@SessionFactory
@SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "Postgres has its own UUID type")
public class UUIDCharTest {
public class UuidAsCharAnnotationTest {
private static class UUIDPair {
UUID rootId;

View File

@ -0,0 +1,127 @@
/*
* 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.id.uuid.sqlrep.sqlchar;
import java.sql.Types;
import java.util.List;
import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.mapping.Property;
import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.BasicType;
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.DomainModelScope;
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 static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
/**
* @author Steve Ebersole
*/
@ServiceRegistry( settings = @Setting( name = AvailableSettings.PREFERRED_UUID_JDBC_TYPE, value = "CHAR" ) )
@DomainModel(annotatedClasses = { UuidAsCharSettingTest.Node.class })
@SessionFactory
public class UuidAsCharSettingTest {
private static class UUIDPair {
UUID rootId;
UUID childId;
public UUIDPair(UUID rootId, UUID childId) {
this.rootId = rootId;
this.childId = childId;
}
}
@Test
public void testSchema(DomainModelScope domainModelScope) {
domainModelScope.withHierarchy( Node.class, (entityDescriptor) -> {
final JdbcTypeRegistry jdbcTypeRegistry = domainModelScope.getDomainModel()
.getTypeConfiguration()
.getJdbcTypeRegistry();
final Property identifierProperty = entityDescriptor.getIdentifierProperty();
final BasicType<?> uuidType = (BasicType<?>) identifierProperty.getType();
assertThat( uuidType.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( Types.CHAR ) ) );
} );
}
@Test
public void testUsage(SessionFactoryScope scope) {
final MappingMetamodel domainModel = scope.getSessionFactory().getRuntimeMetamodels().getMappingMetamodel();
final EntityPersister entityDescriptor = domainModel.findEntityDescriptor( Node.class );
final List<JdbcMapping> identifierJdbcMappings = entityDescriptor.getIdentifierMapping().getJdbcMappings();
assertThat( identifierJdbcMappings, hasSize( 1 ) );
final JdbcMapping jdbcMapping = identifierJdbcMappings.get( 0 );
assertThat( jdbcMapping.getJdbcType().isString(), is( true ) );
final UUIDPair uuidPair = scope.fromTransaction( session -> {
final Node root = new Node( "root" );
session.save( root );
assertThat( root.id, notNullValue());
final Node child = new Node( "child", root );
session.save( child );
assertThat( child.id, notNullValue() );
return new UUIDPair( root.id, child.id );
} );
scope.inTransaction( session -> {
final Node root = session.get( Node.class, uuidPair.rootId );
assertThat( root, notNullValue() );
final Node child = session.get( Node.class, uuidPair.childId );
assertThat( child, notNullValue() );
} );
scope.inTransaction( session -> {
final Node node = session.createQuery( "from Node n join fetch n.parent where n.parent is not null", Node.class ).uniqueResult();
assertThat( node, notNullValue() );
assertThat( node.parent, notNullValue() );
} );
}
@Entity(name = "Node")
static class Node {
@Id
@GeneratedValue
UUID id;
String name;
@ManyToOne
Node parent;
Node() {
}
Node(String name) {
this.name = name;
}
Node(String name, Node parent) {
this.name = name;
this.parent = parent;
}
}
}

View File

@ -280,7 +280,7 @@ In either case, schema validation errors could occur as 5.x used the type code `
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`
To retain backwards compatibility, configure the setting `hibernate.type.preferred_duration_jdbc_type` to `2`
which stands for `Types.NUMERIC`.
[[query]]