From b5220ff9297385ce63ec3e1d00d242e308fe6cab Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 30 Mar 2023 10:36:48 +0200 Subject: [PATCH] HHH-16388 - Configuration setting for wrapper Byte[]/Character[] treatment --- .../boot/internal/MetadataBuilderImpl.java | 27 ++ .../process/spi/MetadataBuildingProcess.java | 19 ++ ...ractDelegatingMetadataBuildingOptions.java | 6 + .../boot/spi/MetadataBuildingOptions.java | 8 + .../org/hibernate/mapping/BasicValue.java | 13 + .../java/org/hibernate/mapping/Property.java | 20 +- .../org/hibernate/type/BasicTypeRegistry.java | 12 + .../dataTypes/BasicOperationsTest.java | 6 + .../orm/test/annotations/lob/ImageTest.java | 9 + .../mapping/basic/ByteArrayMappingTests.java | 6 + .../basic/CharacterArrayMappingTests.java | 6 + ...haracterArrayNationalizedMappingTests.java | 6 + .../WrapperArrayHandlingDisallowTests.java | 55 ++++ .../WrapperArrayHandlingLegacyTests.java | 250 ++++++++++++++++++ migration-guide.adoc | 22 -- 15 files changed, 442 insertions(+), 23 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingDisallowTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java index 45ea63458e..c105fc1961 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java @@ -78,6 +78,7 @@ import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.spi.ServiceException; import org.hibernate.type.BasicType; +import org.hibernate.type.WrapperArrayHandling; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.usertype.UserType; @@ -87,6 +88,8 @@ import jakarta.persistence.AttributeConverter; import jakarta.persistence.ConstraintMode; import jakarta.persistence.SharedCacheMode; +import static org.hibernate.cfg.AvailableSettings.WRAPPER_ARRAY_HANDLING; + /** * @author Steve Ebersole */ @@ -582,6 +585,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont private final MappingDefaultsImpl mappingDefaults; private final IdentifierGeneratorFactory identifierGeneratorFactory; private final TimeZoneStorageType defaultTimezoneStorage; + private final WrapperArrayHandling wrapperArrayHandling; // todo (6.0) : remove bootstrapContext property along with the deprecated methods private BootstrapContext bootstrapContext; @@ -619,6 +623,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont this.mappingDefaults = new MappingDefaultsImpl( serviceRegistry ); this.defaultTimezoneStorage = resolveTimeZoneStorageStrategy( configService ); + this.wrapperArrayHandling = resolveWrapperArrayHandling( configService ); this.multiTenancyEnabled = JdbcEnvironmentImpl.isMultiTenancyEnabled( serviceRegistry ); this.xmlMappingEnabled = configService.getSetting( @@ -868,6 +873,11 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont } } + @Override + public WrapperArrayHandling getWrapperArrayHandling() { + return wrapperArrayHandling; + } + @Override public List getBasicTypeRegistrations() { return basicTypeRegistrations; @@ -999,4 +1009,21 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont TimeZoneStorageType.DEFAULT ); } + + private static WrapperArrayHandling resolveWrapperArrayHandling( + ConfigurationService configService) { + return configService.getSetting( + WRAPPER_ARRAY_HANDLING, + value -> { + if ( value == null ) { + throw new IllegalArgumentException( "Null value passed to convert" ); + } + + return value instanceof WrapperArrayHandling + ? (WrapperArrayHandling) value + : WrapperArrayHandling.valueOf( value.toString().toUpperCase( Locale.ROOT ) ); + }, + WrapperArrayHandling.DISALLOW + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java index 39e4fd795e..3465b3c5e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java @@ -63,6 +63,10 @@ import org.hibernate.mapping.Table; import org.hibernate.type.BasicType; import org.hibernate.type.BasicTypeRegistry; import org.hibernate.type.SqlTypes; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.WrapperArrayHandling; +import org.hibernate.type.descriptor.java.ByteArrayJavaType; +import org.hibernate.type.descriptor.java.CharacterArrayJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonAsStringJdbcType; @@ -603,6 +607,21 @@ public class MetadataBuildingProcess { } }; + if ( options.getWrapperArrayHandling() == WrapperArrayHandling.LEGACY ) { + typeConfiguration.getJavaTypeRegistry().addDescriptor( ByteArrayJavaType.INSTANCE ); + typeConfiguration.getJavaTypeRegistry().addDescriptor( CharacterArrayJavaType.INSTANCE ); + final BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); + + basicTypeRegistry.addTypeReferenceRegistrationKey( + StandardBasicTypes.CHARACTER_ARRAY.getName(), + Character[].class.getName(), "Character[]" + ); + basicTypeRegistry.addTypeReferenceRegistrationKey( + StandardBasicTypes.BINARY_WRAPPER.getName(), + Byte[].class.getName(), "Byte[]" + ); + } + // add Dialect contributed types final Dialect dialect = options.getServiceRegistry().getService( JdbcServices.class ).getDialect(); dialect.contribute( typeContributions, options.getServiceRegistry() ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingMetadataBuildingOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingMetadataBuildingOptions.java index 663e8d7a4a..e8f1df0e24 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingMetadataBuildingOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingMetadataBuildingOptions.java @@ -19,6 +19,7 @@ import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cfg.MetadataSourceType; import org.hibernate.dialect.TimeZoneSupport; import org.hibernate.id.factory.IdentifierGeneratorFactory; +import org.hibernate.type.WrapperArrayHandling; import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.SharedCacheMode; @@ -67,6 +68,11 @@ public abstract class AbstractDelegatingMetadataBuildingOptions implements Metad return delegate.getTimeZoneSupport(); } + @Override + public WrapperArrayHandling getWrapperArrayHandling() { + return delegate.getWrapperArrayHandling(); + } + @Override public List getBasicTypeRegistrations() { return delegate.getBasicTypeRegistrations(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/MetadataBuildingOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/MetadataBuildingOptions.java index 4f03b53245..b887810117 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/MetadataBuildingOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/MetadataBuildingOptions.java @@ -23,6 +23,7 @@ import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.metamodel.internal.ManagedTypeRepresentationResolverStandard; import org.hibernate.metamodel.spi.ManagedTypeRepresentationResolver; +import org.hibernate.type.WrapperArrayHandling; import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.SharedCacheMode; @@ -70,6 +71,13 @@ public interface MetadataBuildingOptions { */ TimeZoneSupport getTimeZoneSupport(); + /** + * @return the {@link WrapperArrayHandling} to use for wrapper arrays {@code Byte[]} and {@code Character[]}. + * + * @see org.hibernate.cfg.AvailableSettings#WRAPPER_ARRAY_HANDLING + */ + WrapperArrayHandling getWrapperArrayHandling(); + default ManagedTypeRepresentationResolver getManagedTypeRepresentationResolver() { // for now always return the standard one return ManagedTypeRepresentationResolverStandard.INSTANCE; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index 8f8b792639..9490637b7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -47,6 +47,7 @@ import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.BasicType; import org.hibernate.type.CustomType; import org.hibernate.type.Type; +import org.hibernate.type.WrapperArrayHandling; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.BasicJavaType; import org.hibernate.type.descriptor.java.BasicPluralJavaType; @@ -857,6 +858,18 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol return visitor.accept(this); } + @Internal + public boolean isDisallowedWrapperArray() { + return getBuildingContext().getBuildingOptions().getWrapperArrayHandling() == WrapperArrayHandling.DISALLOW + && ( explicitJavaTypeAccess == null || explicitJavaTypeAccess.apply( getTypeConfiguration() ) == null ) + && isWrapperByteOrCharacterArray(); + } + + private boolean isWrapperByteOrCharacterArray() { + final Class javaTypeClass = getResolution().getDomainJavaType().getJavaTypeClass(); + return javaTypeClass == Byte[].class || javaTypeClass == Character[].class; + } + /** * Resolved form of {@link BasicValue} as part of interpreting the * boot-time model into the run-time model diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java index 003f3ec48e..ac76167d22 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -18,6 +18,7 @@ import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.boot.model.relational.Database; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadeStyles; import org.hibernate.engine.spi.Mapping; @@ -33,6 +34,7 @@ import org.hibernate.generator.Generator; import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; +import org.hibernate.type.WrapperArrayHandling; /** * A mapping model object representing a property or field of an {@linkplain PersistentClass entity} @@ -282,7 +284,23 @@ public class Property implements Serializable, MetaAttributable { } public boolean isValid(Mapping mapping) throws MappingException { - return getValue().isValid( mapping ); + final Value value = getValue(); + if ( value instanceof BasicValue && ( (BasicValue) value ).isDisallowedWrapperArray() ) { + throw new MappingException( + "The property " + persistentClass.getEntityName() + "#" + name + + " uses a wrapper type Byte[]/Character[] which indicates an issue in your domain model. " + + "These types have been treated like byte[]/char[] until Hibernate 6.2 which meant that " + + "null elements were not allowed, but on JDBC were processed like VARBINARY or VARCHAR. " + + "If you don't use nulls in your arrays, change the type of the property to byte[]/char[]. " + + "To allow explicit uses of the wrapper types Byte[]/Character[] which allows null element " + + "but has a different serialization format than before Hibernate 6.2, configure the " + + "setting " + AvailableSettings.WRAPPER_ARRAY_HANDLING + " to the value " + WrapperArrayHandling.ALLOW + ". " + + "To revert to the legacy treatment of these types, configure the value to " + WrapperArrayHandling.LEGACY + ". " + + "For more information on this matter, consult the migration guide of Hibernate 6.2 " + + "and the Javadoc of the org.hibernate.cfg.AvailableSettings.WRAPPER_ARRAY_HANDLING field." + ); + } + return value.isValid( mapping ); } public String toString() { diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java index 6ab3a6fd53..1970dc959a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java @@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import org.hibernate.HibernateException; +import org.hibernate.Internal; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.StringHelper; @@ -277,6 +278,17 @@ public class BasicTypeRegistry implements Serializable { } } + @Internal + public void addTypeReferenceRegistrationKey(String typeReferenceKey, String... additionalTypeReferenceKeys) { + final BasicTypeReference basicTypeReference = typeReferencesByName.get( typeReferenceKey ); + if ( basicTypeReference == null ) { + throw new IllegalArgumentException( "Couldn't find type reference with name: " + typeReferenceKey ); + } + for ( String additionalTypeReferenceKey : additionalTypeReferenceKeys ) { + typeReferencesByName.put( additionalTypeReferenceKey, basicTypeReference ); + } + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // priming diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/dataTypes/BasicOperationsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/dataTypes/BasicOperationsTest.java index be521032b2..b4ec0a8257 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/dataTypes/BasicOperationsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/dataTypes/BasicOperationsTest.java @@ -14,6 +14,7 @@ import java.sql.Statement; import java.util.Date; import java.util.Locale; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgresPlusDialect; @@ -26,8 +27,10 @@ import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.RequiresDialectFeatureGroup; +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.junit.jupiter.api.Assertions.assertEquals; @@ -48,6 +51,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; annotatedClasses = { SomeEntity.class, SomeOtherEntity.class } ) @SessionFactory +@ServiceRegistry( + settings = @Setting(name = AvailableSettings.WRAPPER_ARRAY_HANDLING, value = "ALLOW") +) public class BasicOperationsTest { private static final String SOME_ENTITY_TABLE_NAME = "SOMEENTITY"; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/lob/ImageTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/lob/ImageTest.java index 926db1e92f..66c8da9bd5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/lob/ImageTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/lob/ImageTest.java @@ -9,8 +9,11 @@ package org.hibernate.orm.test.annotations.lob; import java.util.Arrays; import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SybaseDialect; +import org.hibernate.type.WrapperArrayHandling; import org.hibernate.testing.RequiresDialect; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; @@ -28,6 +31,12 @@ import junit.framework.AssertionFailedError; public class ImageTest extends BaseCoreFunctionalTestCase { private static final int ARRAY_SIZE = 10000; + @Override + protected void configure(Configuration configuration) { + super.configure( configuration ); + configuration.setProperty( AvailableSettings.WRAPPER_ARRAY_HANDLING, WrapperArrayHandling.ALLOW.name() ); + } + @Test public void testBoundedLongByteArrayAccess() { byte[] original = buildRecursively(ARRAY_SIZE, true); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java index a0a53788cf..077455c5da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java @@ -13,6 +13,7 @@ import jakarta.persistence.Lob; import jakarta.persistence.Table; import org.hibernate.annotations.JavaType; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; @@ -24,8 +25,10 @@ import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; 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.AfterEach; import org.junit.jupiter.api.Test; @@ -40,6 +43,9 @@ import static org.hamcrest.Matchers.isOneOf; */ @DomainModel(annotatedClasses = ByteArrayMappingTests.EntityOfByteArrays.class) @SessionFactory +@ServiceRegistry( + settings = @Setting(name = AvailableSettings.WRAPPER_ARRAY_HANDLING, value = "ALLOW") +) public class ByteArrayMappingTests { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayMappingTests.java index 52f2f7b8c1..301b946a59 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayMappingTests.java @@ -13,6 +13,7 @@ import jakarta.persistence.Lob; import jakarta.persistence.Table; import org.hibernate.annotations.JavaType; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; @@ -24,8 +25,10 @@ import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; 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 static org.hamcrest.MatcherAssert.assertThat; @@ -39,6 +42,9 @@ import static org.hamcrest.Matchers.isOneOf; */ @DomainModel(annotatedClasses = CharacterArrayMappingTests.EntityWithCharArrays.class) @SessionFactory +@ServiceRegistry( + settings = @Setting(name = AvailableSettings.WRAPPER_ARRAY_HANDLING, value = "ALLOW") +) public class CharacterArrayMappingTests { @Test public void verifyMappings(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayNationalizedMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayNationalizedMappingTests.java index e92461f1dd..a84dabd240 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayNationalizedMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/CharacterArrayNationalizedMappingTests.java @@ -13,6 +13,7 @@ import jakarta.persistence.Table; import org.hibernate.annotations.JavaType; import org.hibernate.annotations.Nationalized; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.NationalizationSupport; import org.hibernate.metamodel.mapping.JdbcMapping; @@ -27,8 +28,10 @@ import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.RequiresDialectFeature; +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.MatcherAssert.assertThat; @@ -45,6 +48,9 @@ import static org.hamcrest.Matchers.isOneOf; @DomainModel(annotatedClasses = CharacterArrayNationalizedMappingTests.EntityWithCharArrays.class) @SessionFactory @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsNationalizedData.class) +@ServiceRegistry( + settings = @Setting(name = AvailableSettings.WRAPPER_ARRAY_HANDLING, value = "ALLOW") +) public class CharacterArrayNationalizedMappingTests { @Test public void verifyMappings(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingDisallowTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingDisallowTests.java new file mode 100644 index 0000000000..e0d27364cb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingDisallowTests.java @@ -0,0 +1,55 @@ +/* + * 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.mapping.basic; + +import org.hibernate.MappingException; +import org.hibernate.internal.util.ExceptionHelper; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Assertions; +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.containsString; + +/** + * Tests for mapping wrapper values + */ +@DomainModel(annotatedClasses = WrapperArrayHandlingDisallowTests.EntityOfByteArrays.class) +@SessionFactory +public class WrapperArrayHandlingDisallowTests { + + @Test + public void verifyByteArrayMappings(SessionFactoryScope scope) { + try { + scope.getSessionFactory(); + Assertions.fail( "Should fail boot validation!" ); + } + catch (Exception e) { + final Throwable rootCause = ExceptionHelper.getRootCause( e ); + Assertions.assertEquals( MappingException.class, rootCause.getClass() ); + assertThat( rootCause.getMessage(), containsString( WrapperArrayHandlingDisallowTests.EntityOfByteArrays.class.getName() + "#wrapper" ) ); + } + } + + @Entity(name = "EntityOfByteArrays") + @Table(name = "EntityOfByteArrays") + public static class EntityOfByteArrays { + @Id + public Integer id; + private Byte[] wrapper; + + public EntityOfByteArrays() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java new file mode 100644 index 0000000000..dcf2d0909b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/WrapperArrayHandlingLegacyTests.java @@ -0,0 +1,250 @@ +/* + * 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.mapping.basic; + +import java.sql.Types; + +import org.hibernate.annotations.Nationalized; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.NationalizationSupport; +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.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.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +/** + * Tests for mapping wrapper values + */ +@DomainModel(annotatedClasses = { + WrapperArrayHandlingLegacyTests.EntityOfByteArrays.class, + WrapperArrayHandlingLegacyTests.EntityWithCharArrays.class, +}) +@SessionFactory +@ServiceRegistry( + settings = @Setting(name = AvailableSettings.WRAPPER_ARRAY_HANDLING, value = "LEGACY") +) +public class WrapperArrayHandlingLegacyTests { + + @Test + public void verifyByteArrayMappings(SessionFactoryScope scope) { + final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + final JdbcTypeRegistry jdbcTypeRegistry = mappingMetamodel.getTypeConfiguration().getJdbcTypeRegistry(); + final EntityPersister entityDescriptor = mappingMetamodel.getEntityDescriptor( WrapperArrayHandlingLegacyTests.EntityOfByteArrays.class); + + { + final BasicAttributeMapping primitive = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("primitive"); + final JdbcMapping jdbcMapping = primitive.getJdbcMapping(); + assertThat(jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(), equalTo(byte[].class)); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcTypeRegistry.getDescriptor( Types.VARBINARY ) ) ); + } + + { + final BasicAttributeMapping primitive = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapper"); + final JdbcMapping jdbcMapping = primitive.getJdbcMapping(); + assertThat(jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(), equalTo(Byte[].class)); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcTypeRegistry.getDescriptor( Types.VARBINARY ) ) ); + } + + { + final BasicAttributeMapping primitive = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("primitiveLob"); + final JdbcMapping jdbcMapping = primitive.getJdbcMapping(); + assertThat(jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(), equalTo(byte[].class)); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcTypeRegistry.getDescriptor( Types.BLOB ) ) ); + } + + { + final BasicAttributeMapping primitive = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapperLob"); + final JdbcMapping jdbcMapping = primitive.getJdbcMapping(); + assertThat(jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(), equalTo(Byte[].class)); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcTypeRegistry.getDescriptor( Types.BLOB ) ) ); + } + + scope.inTransaction( + (session) -> { + session.persist( + new EntityOfByteArrays( 1, "abc".getBytes(), new Byte[] { (byte) 1 }) + ); + } + ); + + scope.inTransaction( + (session) -> session.get( EntityOfByteArrays.class, 1) + ); + } + + @Test + public void verifyCharacterArrayMappings(SessionFactoryScope scope) { + final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + final JdbcTypeRegistry jdbcRegistry = mappingMetamodel.getTypeConfiguration().getJdbcTypeRegistry(); + final EntityPersister entityDescriptor = mappingMetamodel.getEntityDescriptor( WrapperArrayHandlingLegacyTests.EntityWithCharArrays.class); + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("primitive"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcRegistry.getDescriptor( Types.VARCHAR))); + } + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapper"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcRegistry.getDescriptor( Types.VARCHAR))); + } + + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("primitiveClob"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcRegistry.getDescriptor( Types.CLOB))); + } + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapperClob"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), equalTo( jdbcRegistry.getDescriptor( Types.CLOB))); + } + } + + @Test + public void verifyCharacterArrayNationalizedMappings(SessionFactoryScope scope) { + final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + final EntityPersister entityDescriptor = mappingMetamodel.getEntityDescriptor( + WrapperArrayHandlingLegacyTests.EntityWithCharArrays.class); + final JdbcTypeRegistry jdbcTypeRegistry = mappingMetamodel.getTypeConfiguration().getJdbcTypeRegistry(); + + final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); + final NationalizationSupport nationalizationSupport = dialect.getNationalizationSupport(); + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("primitiveNVarchar"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( nationalizationSupport.getVarcharVariantCode()))); + } + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapperNVarchar"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( nationalizationSupport.getVarcharVariantCode()))); + } + + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("primitiveNClob"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( nationalizationSupport.getClobVariantCode()))); + } + + { + final BasicAttributeMapping attributeMapping = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapperNClob"); + final JdbcMapping jdbcMapping = attributeMapping.getJdbcMapping(); + assertThat( jdbcMapping.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( nationalizationSupport.getClobVariantCode()))); + } + } + + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + session.createMutationQuery("delete EntityOfByteArrays").executeUpdate(); + session.createMutationQuery("delete EntityWithCharArrays").executeUpdate(); + } + ); + } + + @Entity(name = "EntityOfByteArrays") + @Table(name = "EntityOfByteArrays") + public static class EntityOfByteArrays { + @Id + public Integer id; + + //tag::basic-bytearray-example[] + // mapped as VARBINARY + private byte[] primitive; + private Byte[] wrapper; + + // mapped as (materialized) BLOB + @Lob + private byte[] primitiveLob; + @Lob + private Byte[] wrapperLob; + //end::basic-bytearray-example[] + + public EntityOfByteArrays() { + } + + public EntityOfByteArrays(Integer id, byte[] primitive, Byte[] wrapper) { + this.id = id; + this.primitive = primitive; + this.wrapper = wrapper; + this.primitiveLob = primitive; + this.wrapperLob = wrapper; + } + + public EntityOfByteArrays(Integer id, byte[] primitive, Byte[] wrapper, byte[] primitiveLob, Byte[] wrapperLob) { + this.id = id; + this.primitive = primitive; + this.wrapper = wrapper; + this.primitiveLob = primitiveLob; + this.wrapperLob = wrapperLob; + } + } + @Entity(name = "EntityWithCharArrays") + @Table(name = "EntityWithCharArrays") + public static class EntityWithCharArrays { + @Id + public Integer id; + + // mapped as VARCHAR + char[] primitive; + Character[] wrapper; + + // mapped as CLOB + @Lob + char[] primitiveClob; + @Lob + Character[] wrapperClob; + + // mapped as NVARCHAR + @Nationalized + char[] primitiveNVarchar; + @Nationalized + Character[] wrapperNVarchar; + + // mapped as NCLOB + @Lob + @Nationalized + char[] primitiveNClob; + @Lob + @Nationalized + Character[] wrapperNClob; + } +} diff --git a/migration-guide.adoc b/migration-guide.adoc index 36f1210a48..7b859e77c0 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -14,25 +14,3 @@ earlier versions, see any other pertinent migration guides as well. * link:{docsBase}/6.1/migration-guide/migration-guide.html[6.1 Migration guide] * link:{docsBase}/6.0/migration-guide/migration-guide.html[6.0 Migration guide] -[[ddl-changes]] -== DDL type changes - -[[ddl-offset-time]] -=== OffsetTime mapping changes - -`OffsetTime` now depends on `@TimeZoneStorage` and the `hibernate.timezone.default_storage` setting. -Since the default for this setting is now `TimeZoneStorageType.DEFAULT`, this means that the DDL expectations for such columns changed. - -If the target database supports time zone types natively like H2, Oracle, SQL Server and DB2 z/OS, -the type code `SqlTypes.TIME_WITH_TIMEZONE` is now used, which maps to the DDL type `time with time zone`. - -Due to this change, schema validation errors could occur on existing databases. - -The migration to `time with time zone` requires a migration expression like `cast(old as time with time zone)` -which will interpret the previous time as local time and compute the offset for the `time with time zone` based on the current date -and time zone settings of your database session. - -If the target database does not support time zone types natively, Hibernate behaves just like before. - -To retain backwards compatibility, configure the setting `hibernate.timezone.default_storage` to `NORMALIZE`. -