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 97d04b32706d..6bb97c93a27c 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -768,19 +768,25 @@ private JavaType specialJavaType( // and implement toString/fromString as well as copying based on FormatMapper operations switch ( jdbcTypeCode ) { case SqlTypes.JSON: - final JavaType jsonJavaType = - new JsonJavaType<>( impliedJavaType, + return javaTypeRegistry.resolveDescriptor( + SqlTypes.JSON, + impliedJavaType, + () -> new JsonJavaType<>( + impliedJavaType, mutabilityPlan( typeConfiguration, impliedJavaType ), - typeConfiguration ); - javaTypeRegistry.addDescriptor( jsonJavaType ); - return jsonJavaType; + typeConfiguration + ) + ); case SqlTypes.SQLXML: - final JavaType xmlJavaType = - new XmlJavaType<>( impliedJavaType, + return javaTypeRegistry.resolveDescriptor( + SqlTypes.SQLXML, + impliedJavaType, + () -> new XmlJavaType<>( + impliedJavaType, mutabilityPlan( typeConfiguration, impliedJavaType ), - typeConfiguration ); - javaTypeRegistry.addDescriptor( xmlJavaType ); - return xmlJavaType; + typeConfiguration + ) + ); } } return javaTypeRegistry.resolveDescriptor( impliedJavaType ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java index 88678b0582de..6262e4f07483 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java @@ -12,6 +12,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.type.descriptor.java.ArrayJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -35,6 +36,7 @@ public class JavaTypeRegistry implements JavaTypeBaseline.BaselineTarget, Serial private final TypeConfiguration typeConfiguration; private final ConcurrentHashMap> descriptorsByTypeName = new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> typeCodeSpecificDescriptorsByTypeName = new ConcurrentHashMap<>(); public JavaTypeRegistry(TypeConfiguration typeConfiguration) { this.typeConfiguration = typeConfiguration; @@ -72,6 +74,9 @@ private void performInjections(JavaType descriptor) { public void forEachDescriptor(Consumer> consumer) { descriptorsByTypeName.values().forEach( consumer ); + typeCodeSpecificDescriptorsByTypeName.values().forEach( descriptorsByTypeName -> { + descriptorsByTypeName.values().forEach( consumer ); + } ); } public JavaType getDescriptor(Type javaType) { @@ -79,6 +84,16 @@ public JavaType getDescriptor(Type javaType) { } public void addDescriptor(JavaType descriptor) { + addDescriptor( descriptorsByTypeName, descriptor ); + } + + public void addDescriptor(int sqlTypeCode, JavaType descriptor) { + final ConcurrentHashMap> descriptorsByTypeName = + typeCodeSpecificDescriptorsByTypeName.computeIfAbsent( sqlTypeCode, k -> new ConcurrentHashMap<>() ); + addDescriptor( descriptorsByTypeName, descriptor ); + } + + private void addDescriptor(ConcurrentHashMap> descriptorsByTypeName, JavaType descriptor) { final JavaType old = descriptorsByTypeName.put( descriptor.getJavaType().getTypeName(), descriptor ); if ( old != null ) { log.debugf( @@ -91,23 +106,40 @@ public void addDescriptor(JavaType descriptor) { performInjections( descriptor ); } - public JavaType findDescriptor(Type javaType) { + public @Nullable JavaType findDescriptor(Type javaType) { //noinspection unchecked return (JavaType) descriptorsByTypeName.get( javaType.getTypeName() ); } + public @Nullable JavaType findDescriptor(int sqlTypeCode, Type javaType) { + final ConcurrentHashMap> descriptorsByType = + typeCodeSpecificDescriptorsByTypeName.get( sqlTypeCode ); + return descriptorsByType.get( javaType.getTypeName() ); + } + public JavaType resolveDescriptor(Type javaType, Supplier> creator) { - return resolveDescriptor( javaType.getTypeName(), creator ); + //noinspection unchecked + return (JavaType) resolveDescriptor( javaType.getTypeName(), creator ); + } + + public JavaType resolveDescriptor(int sqlTypeCode, Type javaType, Supplier> creator) { + final ConcurrentHashMap> descriptorsByTypeName = + typeCodeSpecificDescriptorsByTypeName.computeIfAbsent( sqlTypeCode, k -> new ConcurrentHashMap<>() ); + return resolveDescriptor( descriptorsByTypeName, javaType.getTypeName(), creator ); + } + + private JavaType resolveDescriptor(String javaTypeName, Supplier> creator) { + //noinspection unchecked + return (JavaType) resolveDescriptor( descriptorsByTypeName, javaTypeName, creator ); } - private JavaType resolveDescriptor(String javaTypeName, Supplier> creator) { - final JavaType cached = descriptorsByTypeName.get( javaTypeName ); + private JavaType resolveDescriptor(ConcurrentHashMap> descriptorsByTypeName, String javaTypeName, Supplier> creator) { + final var cached = descriptorsByTypeName.get( javaTypeName ); if ( cached != null ) { - //noinspection unchecked - return (JavaType) cached; + return cached; } - final JavaType created = creator.get(); + final JavaType created = creator.get(); descriptorsByTypeName.put( javaTypeName, created ); return created; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonAndArrayMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonAndArrayMappingTests.java new file mode 100644 index 000000000000..98766785cd86 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonAndArrayMappingTests.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +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.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isA; + +// It's vital that EntityWithJson is listed first to reproduce the bug, +// the bug being, that JsonJavaType was registered in JavaTypeRegistry under e.g. List, +// which would then wrongly be used for EntityWithArray#listInteger as JavaType +@DomainModel(annotatedClasses = { JsonAndArrayMappingTests.EntityWithJson.class, JsonAndArrayMappingTests.EntityWithArray.class}) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = AvailableSettings.JSON_FORMAT_MAPPER, value = "jackson")) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class) +@Jira("https://hibernate.atlassian.net/browse/HHH-17680") +public class JsonAndArrayMappingTests { + + @Test + public void verifyMappings(SessionFactoryScope scope) { + final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + final JdbcTypeRegistry jdbcTypeRegistry = mappingMetamodel.getTypeConfiguration().getJdbcTypeRegistry(); + final JdbcType jsonType = jdbcTypeRegistry.getDescriptor( SqlTypes.JSON ); + final EntityPersister jsonEntity = mappingMetamodel.findEntityDescriptor( EntityWithJson.class ); + + final BasicAttributeMapping listStringJsonAttribute = (BasicAttributeMapping) jsonEntity.findAttributeMapping( + "listString" ); + final BasicAttributeMapping listIntegerJsonAttribute = (BasicAttributeMapping) jsonEntity.findAttributeMapping( + "listInteger" ); + + assertThat( listStringJsonAttribute.getJavaType().getJavaTypeClass(), equalTo( List.class ) ); + assertThat( listIntegerJsonAttribute.getJavaType().getJavaTypeClass(), equalTo( List.class ) ); + + assertThat( listStringJsonAttribute.getJdbcMapping().getJdbcType(), isA( (Class) jsonType.getClass() ) ); + assertThat( listIntegerJsonAttribute.getJdbcMapping().getJdbcType(), isA( (Class) jsonType.getClass() ) ); + + final EntityPersister arrayEntity = mappingMetamodel.findEntityDescriptor( EntityWithArray.class ); + + final BasicAttributeMapping listStringArrayAttribute = (BasicAttributeMapping) arrayEntity.findAttributeMapping( + "listString" ); + final BasicAttributeMapping listIntegerArrayAttribute = (BasicAttributeMapping) arrayEntity.findAttributeMapping( + "listInteger" ); + + assertThat( listStringArrayAttribute.getJavaType().getJavaTypeClass(), equalTo( List.class ) ); + assertThat( listIntegerArrayAttribute.getJavaType().getJavaTypeClass(), equalTo( List.class ) ); + + assertThat( listStringArrayAttribute.getJdbcMapping().getJdbcType(), isA( ArrayJdbcType.class ) ); + assertThat( listIntegerArrayAttribute.getJdbcMapping().getJdbcType(), isA( ArrayJdbcType.class ) ); + } + + @Entity(name = "EntityWithJson") + @Table(name = "EntityWithJson") + public static class EntityWithJson { + @Id + private Integer id; + + @JdbcTypeCode( SqlTypes.JSON ) + private List listString; + @JdbcTypeCode( SqlTypes.JSON ) + private List listInteger; + + public EntityWithJson() { + } + } + + @Entity(name = "EntityWithArray") + @Table(name = "EntityWithArray") + public static class EntityWithArray { + @Id + private Integer id; + + @JdbcTypeCode( SqlTypes.ARRAY ) + private List listString; + @JdbcTypeCode( SqlTypes.ARRAY ) + private List listInteger; + + public EntityWithArray() { + } + } +}