HHH-16388 - Configuration setting for wrapper Byte[]/Character[] treatment

This commit is contained in:
Christian Beikov 2023-03-30 10:36:48 +02:00
parent b799da7b60
commit 214b647f0f
15 changed files with 456 additions and 14 deletions

View File

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

View File

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

View File

@ -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<BasicTypeRegistration> getBasicTypeRegistrations() {
return delegate.getBasicTypeRegistrations();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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