HHH-16481 - Add a new WrapperArrayHandling for enabled JPA compliance

This commit is contained in:
Steve Ebersole 2023-04-17 16:34:22 -05:00
parent d7c5db47f6
commit 959858f8bc
3 changed files with 193 additions and 24 deletions

View File

@ -61,6 +61,7 @@ import org.hibernate.cache.spi.RegionFactory;
import org.hibernate.cache.spi.access.AccessType;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.MetadataSourceType;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.TimeZoneSupport;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.config.spi.StandardConverters;
@ -70,14 +71,17 @@ import org.hibernate.id.factory.IdentifierGeneratorFactory;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.log.DeprecationLogger;
import org.hibernate.internal.util.NullnessHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.jpa.spi.JpaCompliance;
import org.hibernate.metamodel.CollectionClassification;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
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.SqlTypes;
import org.hibernate.type.WrapperArrayHandling;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.usertype.UserType;
@ -88,7 +92,9 @@ import jakarta.persistence.AttributeConverter;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.SharedCacheMode;
import static org.hibernate.cfg.AvailableSettings.JPA_COMPLIANCE;
import static org.hibernate.cfg.AvailableSettings.WRAPPER_ARRAY_HANDLING;
import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN;
/**
* @author Steve Ebersole
@ -475,7 +481,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
this.implicitlyQuoteIdentifiers = configService.getSetting(
AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS,
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
@ -623,30 +629,30 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
this.mappingDefaults = new MappingDefaultsImpl( serviceRegistry );
this.defaultTimezoneStorage = resolveTimeZoneStorageStrategy( configService );
this.wrapperArrayHandling = resolveWrapperArrayHandling( configService );
this.wrapperArrayHandling = resolveWrapperArrayHandling( configService, serviceRegistry );
this.multiTenancyEnabled = JdbcEnvironmentImpl.isMultiTenancyEnabled( serviceRegistry );
this.xmlMappingEnabled = configService.getSetting(
AvailableSettings.XML_MAPPING_ENABLED,
StandardConverters.BOOLEAN,
BOOLEAN,
true
);
this.implicitDiscriminatorsForJoinedInheritanceSupported = configService.getSetting(
AvailableSettings.IMPLICIT_DISCRIMINATOR_COLUMNS_FOR_JOINED_SUBCLASS,
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
this.explicitDiscriminatorsForJoinedInheritanceSupported = !configService.getSetting(
AvailableSettings.IGNORE_EXPLICIT_DISCRIMINATOR_COLUMNS_FOR_JOINED_SUBCLASS,
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
this.implicitlyForceDiscriminatorInSelect = configService.getSetting(
AvailableSettings.FORCE_DISCRIMINATOR_IN_SELECTS_BY_DEFAULT,
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
@ -710,7 +716,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
this.specjProprietarySyntaxEnabled = configService.getSetting(
"hibernate.enable_specj_proprietary_syntax",
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
@ -760,7 +766,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
this.useNationalizedCharacterData = configService.getSetting(
AvailableSettings.USE_NATIONALIZED_CHARACTER_DATA,
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
@ -772,7 +778,7 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
allowExtensionsInCdi = configService.getSetting(
AvailableSettings.ALLOW_EXTENSIONS_IN_CDI,
StandardConverters.BOOLEAN,
BOOLEAN,
false
);
}
@ -1011,19 +1017,38 @@ public class MetadataBuilderImpl implements MetadataBuilderImplementor, TypeCont
}
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
ConfigurationService configService,
StandardServiceRegistry serviceRegistry) {
final WrapperArrayHandling setting = NullnessHelper.coalesceSuppliedValues(
() -> configService.getSetting(
WRAPPER_ARRAY_HANDLING,
WrapperArrayHandling::interpretExternalSettingLeniently
),
() -> resolveFallbackWrapperArrayHandling( configService, serviceRegistry )
);
if ( setting == WrapperArrayHandling.PICK ) {
final Dialect dialect = serviceRegistry.getService( JdbcServices.class ).getDialect();
if ( dialect.supportsStandardArrays()
&& ( dialect.getPreferredSqlTypeCodeForArray() == SqlTypes.ARRAY
|| dialect.getPreferredSqlTypeCodeForArray() == SqlTypes.SQLXML ) ) {
return WrapperArrayHandling.ALLOW;
}
return WrapperArrayHandling.LEGACY;
}
return setting;
};
private static WrapperArrayHandling resolveFallbackWrapperArrayHandling(
ConfigurationService configService,
StandardServiceRegistry serviceRegistry) {
if ( configService.getSetting( JPA_COMPLIANCE, BOOLEAN ) == Boolean.TRUE ) {
// JPA compliance was enabled. Use PICK
return WrapperArrayHandling.PICK;
}
return WrapperArrayHandling.DISALLOW;
}
}

View File

@ -6,6 +6,9 @@
*/
package org.hibernate.type;
import java.util.Locale;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.type.descriptor.java.ByteArrayJavaType;
import org.hibernate.type.descriptor.java.CharacterArrayJavaType;
@ -21,7 +24,8 @@ public enum WrapperArrayHandling {
/**
* Throw an informative and actionable error if the types are used explicitly in the domain model
*
* @implNote The default behavior
* @implNote The default behavior; unless {@linkplain AvailableSettings#JPA_COMPLIANCE JPA compliance}
* is enabled - see {@linkplain #PICK}
*/
DISALLOW,
@ -42,5 +46,37 @@ public enum WrapperArrayHandling {
* @apiNote Hibernate recommends users who want the legacy semantic change the domain model to use
* {@code byte[]} and {@code char[]} rather than using this setting.
*/
LEGACY
LEGACY,
/**
* Hibernate will pick between {@linkplain #ALLOW} and {@linkplain #LEGACY} depending on
* whether the Dialect supports SQL {@code ARRAY} types.
*
* @implNote The default if {@linkplain AvailableSettings#JPA_COMPLIANCE JPA compliance} is enabled.
*/
PICK;
public static WrapperArrayHandling interpretExternalSetting(Object value) {
if ( value == null ) {
throw new IllegalArgumentException( "Null value passed to convert" );
}
return value instanceof WrapperArrayHandling
? (WrapperArrayHandling) value
: valueOf( value.toString().toUpperCase( Locale.ROOT ) );
}
/**
* Form of {@link #interpretExternalSetting(Object)} which allows incoming {@code null} values and
* simply returns {@code null}. Useful for chained resolutions
*/
public static WrapperArrayHandling interpretExternalSettingLeniently(Object value) {
if ( value == null ) {
return null;
}
return value instanceof WrapperArrayHandling
? (WrapperArrayHandling) value
: valueOf( value.toString().toUpperCase( Locale.ROOT ) );
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.jpa.mapping;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import static org.junit.jupiter.api.Assertions.fail;
/**
* @author Steve Ebersole
*/
public class WrapperArrayHandlingTests {
@Test
@ServiceRegistry(
settings = @Setting( name = AvailableSettings.JPA_COMPLIANCE, value = "true" )
)
void testComplianceEnabled(ServiceRegistryScope scope) {
try ( SessionFactory sessionFactory = buildSessionFactory( scope ) ) {
// we expect this one to pass
}
}
private SessionFactory buildSessionFactory(ServiceRegistryScope scope) {
final MetadataSources metadataSources = new MetadataSources( scope.getRegistry() );
final Metadata metadata = metadataSources.addAnnotatedClasses( TheEntity.class ).buildMetadata();
return metadata.buildSessionFactory();
}
@Test
@ServiceRegistry(
settings = @Setting( name = AvailableSettings.JPA_COMPLIANCE, value = "false" )
)
void testComplianceDisabled(ServiceRegistryScope scope) {
try ( SessionFactory sessionFactory = buildSessionFactory( scope ) ) {
// however, this one should fall because DISALLOW is the default
fail( "Expecting an exception due to DISALLOW" );
}
catch (Exception expected) {
}
}
@Entity( name = "TheEntity" )
@Table( name = "TheEntity" )
public static class TheEntity {
@Id
private Integer id;
@Basic
private String name;
@Basic
private Character[] characters;
@Basic
private Byte[] bytes;
protected TheEntity() {
// for use by Hibernate
}
public TheEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Character[] getCharacters() {
return characters;
}
public void setCharacters(Character[] characters) {
this.characters = characters;
}
public Byte[] getBytes() {
return bytes;
}
public void setBytes(Byte[] bytes) {
this.bytes = bytes;
}
}
}