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 7c0bb115d6..4cbb9f5071 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -114,22 +114,23 @@ To revert to Hibernate ORM 5's behavior, set the configuration property `hiberna [[byte-and-character-array-mapping-changes]] == Byte[]/Character[] mapping changes -The mappings for the wrapper array types `Byte[]` and `Character[]` have changed. +Hibernate historically allowed mapping `Byte[]` and `Character[]` in a domain model as basic values to +`VARBINARY` and `(N)VARCHAR` SQL types. -Prior to Hibernate 6.2, a `Byte[]` in the domain model was always just blindly converted to a `byte[]`, -possibly running into a `NullPointerException` if the `Byte[]` contains a `null` array element. -Similarly, `Character[]` was blindly converted to `char[]`, potentially running into the same `NullPointerException` problem. +Strictly speaking, this is an inaccurate mapping. Because the Java wrapper types (`Byte` and `Character`) are used, null +elements are allowed. However, it is not possible to store such domain values as `VARBINARY` and `(N)VARCHAR` SQL types. +In fact, attempting to store such values leads to errors on previous versions. The legacy support has an implicit contract +that the `Byte[]` and `Character[]` types are handled exactly the same as the `byte[]` and `char[]` variants. -Since disallowing `null` elements defeats the purpose of choosing the wrapper array type in the first place, -the Hibernate team concluded that changing the mapping for these types is fine, -because there can not be any reasonable real world uses of `Byte[]` and `Character[]`. +Building on the link:{docsBase}/6.1/migration-guide/migration-guide.html#basic-arraycollection-mapping[ability] to use +structured SQL types (`ARRAY`, `SQLXML`, ...) for storing basic values, 6.2 makes it configurable how to handle mappings of +this type: -Another motivation for fixing this is that array handling code currently has to special case the element types -`Byte` and `Character`, and that binding SQL arrays of these types is not possible for native queries. +DISALLOW:: (default) Throw an informative and actionable error +ALLOW:: Allows the use of the wrapper arrays stored as structured SQL types (`ARRAY`, `SQLXML`, ...) to maintain proper null element semantics. +LEGACY:: Allows the use of the wrapper arrays stored as `VARBINARY` and `VARCHAR`, disallowing null elements. -Starting with Hibernate 6.2, `Byte[]` and `Character[]` will be treated like other arrays. -See the link:{docsBase}/6.1/migration-guide/migration-guide.html#basic-arraycollection-mapping[6.1 Migration guide] for -details about the default DDL mapping. +See link:{javadocsBase}/org/hibernate/cfg/AvailableSettings.html#WRAPPER_ARRAY_HANDLING[AvailableSettings#WRAPPER_ARRAY_HANDLING] A possible migration could involve the following steps in a migration script: @@ -139,7 +140,7 @@ A possible migration could involve the following steps in a migration script: * For every result, load the Hibernate entity by primary key and set the field value to transformed result `Byte[]` or `Character[]` * Finally, drop the old column `alter table tbl drop column array_col_old` -Alternatively, to revert to pre-6.2 behavior, annotate your array property with `@JavaType(ByteArrayJavaType.class)` +Alternatively, to revert to pre-6.2 behavior for specific properties, annotate your array property with `@JavaType(ByteArrayJavaType.class)` or `@JavaType(CharacterArrayJavaType.class)` or simply change the domain model type to `byte[]` and `char[]` respectively. [[ddl-check]]