diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index d23fec16e188..6a1efd8764bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -333,6 +333,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.toSmallList; import static org.hibernate.loader.ast.internal.MultiKeyLoadHelper.supportsSqlArrayType; import static org.hibernate.metamodel.RepresentationMode.POJO; +import static org.hibernate.metamodel.mapping.MappingModelHelper.isCompatibleModelPart; import static org.hibernate.persister.entity.DiscriminatorHelper.NOT_NULL_DISCRIMINATOR; import static org.hibernate.persister.entity.DiscriminatorHelper.NULL_DISCRIMINATOR; import static org.hibernate.pretty.MessageHelper.infoString; @@ -6219,50 +6220,30 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { } } - if ( treatTargetType != null ) { - if ( ! treatTargetType.isTypeOrSuperType( this ) ) { + if ( treatTargetType == null ) { + final var subDefinedAttribute = findSubPartInSubclassMappings( name ); + if ( subDefinedAttribute != null ) { + return subDefinedAttribute; + } + } + else if ( treatTargetType != this ) { + if ( !treatTargetType.isTypeOrSuperType( this ) ) { return null; } - - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - if ( ! treatTargetType.isTypeOrSuperType( subMappingType ) ) { - continue; - } - - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - - if ( subDefinedAttribute != null ) { - return subDefinedAttribute; - } - } + // Prefer attributes defined in the treat target type or its subtypes + final var treatTypeSubPart = treatTargetType.findSubTypesSubPart( name, null ); + if ( treatTypeSubPart != null ) { + return treatTypeSubPart; } - } - else { - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - ModelPart attribute = null; - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - if ( subDefinedAttribute != null ) { - if ( attribute != null && !MappingModelHelper.isCompatibleModelPart( attribute, subDefinedAttribute ) ) { - throw new PathException( - String.format( - Locale.ROOT, - "Could not resolve attribute '%s' of '%s' due to the attribute being declared in multiple subtypes '%s' and '%s'", - name, - getJavaType().getTypeName(), - attribute.asAttributeMapping().getDeclaringType() - .getJavaType().getTypeName(), - subDefinedAttribute.asAttributeMapping().getDeclaringType() - .getJavaType().getTypeName() - ) - ); - } - attribute = subDefinedAttribute; + else { + // If not found, look in the treat target type's supertypes + EntityMappingType superType = treatTargetType.getSuperMappingType(); + while ( superType != this ) { + final var superTypeSubPart = superType.findDeclaredAttributeMapping( name ); + if ( superTypeSubPart != null ) { + return superTypeSubPart; } - } - if ( attribute != null ) { - return attribute; + superType = superType.getSuperMappingType(); } } } @@ -6285,6 +6266,29 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { } } + private ModelPart findSubPartInSubclassMappings(String name) { + ModelPart attribute = null; + if ( isNotEmpty( subclassMappingTypes ) ) { + for ( var subMappingType : subclassMappingTypes.values() ) { + final var subDefinedAttribute = subMappingType.findSubTypesSubPart( name, null ); + if ( subDefinedAttribute != null ) { + if ( attribute != null && !isCompatibleModelPart( attribute, subDefinedAttribute ) ) { + throw new PathException( String.format( + Locale.ROOT, + "Could not resolve attribute '%s' of '%s' due to the attribute being declared in multiple subtypes '%s' and '%s'", + name, + getJavaType().getTypeName(), + attribute.asAttributeMapping().getDeclaringType().getJavaType().getTypeName(), + subDefinedAttribute.asAttributeMapping().getDeclaringType().getJavaType().getTypeName() + ) ); + } + attribute = subDefinedAttribute; + } + } + } + return attribute; + } + @Override public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetType) { final AttributeMapping declaredAttribute = declaredAttributeMappings.get( name ); @@ -6292,15 +6296,7 @@ public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetT return declaredAttribute; } else { - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - if ( subDefinedAttribute != null ) { - return subDefinedAttribute; - } - } - } - return null; + return findSubPartInSubclassMappings( name ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java new file mode 100644 index 000000000000..6e866e58f0f7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.discriminator; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + JoinedDiscSameAttributeNameTest.Ancestor.class, + JoinedDiscSameAttributeNameTest.DescendantA.class, + JoinedDiscSameAttributeNameTest.DescendantB.class, + JoinedDiscSameAttributeNameTest.DescendantTak.class, + JoinedDiscSameAttributeNameTest.DescendantD.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19756" ) +public class JoinedDiscSameAttributeNameTest { + @Test + void testCoalesceSameType(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createTupleQuery(); + final var root = query.from( Ancestor.class ); + final var dscCRoot = cb.treat( root, DescendantTak.class ); + + query.select( cb.tuple( + root.get( "id" ).alias( "id" ), + cb.coalesce( + dscCRoot.get( "subtitle" ), + dscCRoot.get( "title" ) + ).alias( "description" ) + ) ).orderBy( cb.asc( root.get( "id" ) ) ); + + final var resultList = session.createSelectionQuery( query ).getResultList(); + assertResults( resultList, null, "title", null ); + } ); + } + + @Test + void testCoalesceDifferentTypes(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createTupleQuery(); + final var root = query.from( Ancestor.class ); + final var dscARoot = cb.treat( root, DescendantA.class ); + final var dscCRoot = cb.treat( root, DescendantTak.class ); + final var dscDRoot = cb.treat( root, DescendantD.class ); + + query.select( cb.tuple( + root.get( "id" ).alias( "id" ), + cb.coalesce( + dscDRoot.get( "subtitle" ), + cb.coalesce( + cb.coalesce( + dscARoot.get( "subtitle" ), + dscARoot.get( "title" ) + ), + cb.coalesce( + dscCRoot.get( "subtitle" ), + dscCRoot.get( "title" ) + ) + ) + ).alias( "description" ) + ) ).orderBy( cb.asc( root.get( "id" ) ) ); + + final var resultList = session.createSelectionQuery( query ).getResultList(); + assertResults( resultList, null, "title", "subtitle" ); + } ); + } + + private static void assertResults(List resultList, String... expected) { + assertThat( resultList ).hasSize( expected.length ); + for ( int i = 0; i < expected.length; i++ ) { + final var r = resultList.get( i ); + assertThat( r.get( 0, Integer.class) ).isEqualTo( i + 1 ); + assertThat( r.get( 1, String.class ) ).isEqualTo( expected[i] ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var descendantA = new DescendantA(); + descendantA.id = 1; + session.persist( descendantA ); + final var descendantTak = new DescendantTak(); + descendantTak.id = 2; + descendantTak.title = "title"; + session.persist( descendantTak ); + final var descendantD = new DescendantD(); + descendantD.id = 3; + descendantD.subtitle = "subtitle"; + session.persist( descendantD ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Ancestor") + @Table(name = "t_ancestor") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name = "def_type_id") + static abstract class Ancestor { + @Id + Integer id; + } + + @Entity(name = "DescendantA") + @DiscriminatorValue("A") + @Table(name = "t_descendant_a") + static class DescendantA extends Ancestor { + String title; + String subtitle; + } + + @Entity(name = "DescendantB") + @Table(name = "t_descendant_b") + static abstract class DescendantB extends Ancestor { + } + + @Entity(name = "DescendantTak") + @DiscriminatorValue("C") + @Table(name = "t_descendant_c") + static class DescendantTak extends DescendantB { + String title; + String subtitle; + } + + @Entity(name = "DescendantD") + @DiscriminatorValue("D") + @Table(name = "t_descendant_d") + static class DescendantD extends DescendantB { + String subtitle; + } +}