From ec4d1ac06c4769573de70d273c942b2f9e64d971 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 19 Aug 2025 17:45:22 +0200 Subject: [PATCH] HHH-19687 Correctly instantiate id for circular key-to-one fetch within embedded id --- .../internal/AbstractEmbeddableMapping.java | 1 + .../internal/MappingModelCreationHelper.java | 13 +- .../internal/ToOneAttributeMapping.java | 94 +++++++++----- ...beddedIdLazyOneToOneCriteriaQueryTest.java | 119 ++++++++++++++++++ 4 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java index 7fd9d2d0d306..5bf61af5087d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java @@ -245,6 +245,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping ) { creationProcess ) ); + toOne.setupCircularFetchModelPart( creationProcess ); attributeMapping = toOne; currentIndex += attributeMapping.getJdbcTypeCount(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index f95d25ece29c..17cf1a06cc56 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -893,7 +893,8 @@ public static boolean interpretToOneKeyDescriptor( return interpretNestedToOneKeyDescriptor( referencedEntityDescriptor, referencedPropertyName, - attributeMapping + attributeMapping, + creationProcess ); } @@ -921,6 +922,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart ) { creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); } else if ( modelPart == null ) { throw new IllegalArgumentException( "Unable to find attribute " + bootProperty.getPersistentClass() @@ -1017,6 +1019,7 @@ else if ( modelPart == null ) { swapDirection ); attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor ); } else if ( fkTarget instanceof EmbeddableValuedModelPart ) { @@ -1033,6 +1036,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) { creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor ); } else { @@ -1053,13 +1057,15 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) { * @param referencedEntityDescriptor The entity which contains the inverse property * @param referencedPropertyName The inverse property name path * @param attributeMapping The attribute for which we try to set the foreign key + * @param creationProcess The creation process * @return true if the foreign key is actually set */ private static boolean interpretNestedToOneKeyDescriptor( EntityPersister referencedEntityDescriptor, String referencedPropertyName, - ToOneAttributeMapping attributeMapping) { - String[] propertyPath = StringHelper.split( ".", referencedPropertyName ); + ToOneAttributeMapping attributeMapping, + MappingModelCreationProcess creationProcess) { + final String[] propertyPath = StringHelper.split( ".", referencedPropertyName ); EmbeddableValuedModelPart lastEmbeddableModelPart = null; for ( int i = 0; i < propertyPath.length; i++ ) { @@ -1084,6 +1090,7 @@ private static boolean interpretNestedToOneKeyDescriptor( } attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); return true; } if ( modelPart instanceof EmbeddableValuedModelPart ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index e77d8d182e7b..613f33ed7853 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -41,6 +41,7 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMetadata; import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; @@ -178,6 +179,7 @@ public class Entity1 { private ForeignKeyDescriptor.Nature sideNature; private String identifyingColumnsTableExpression; private boolean canUseParentTableGroup; + private @Nullable EmbeddableValuedModelPart circularFetchModelPart; /** * For Hibernate Reactive @@ -868,6 +870,29 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); } + public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) { + final EntityIdentifierMapping entityIdentifierMapping = getAssociatedEntityMappingType().getIdentifierMapping(); + if ( sideNature == ForeignKeyDescriptor.Nature.TARGET + && entityIdentifierMapping instanceof CompositeIdentifierMapping + && foreignKeyDescriptor.getKeyPart() != entityIdentifierMapping ) { + // Setup a special embeddable model part for fetching the key object for a circular fetch. + // This is needed if the association entity nests the "inverse" toOne association in the embedded id, + // because then, the key part of the foreign key is just a simple value instead of the expected embedded id + // when doing delayed creation/querying of target entities. See HHH-19687 for details + final CompositeIdentifierMapping identifierMapping = (CompositeIdentifierMapping) entityIdentifierMapping; + this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart( + identifierMapping, + getDeclaringType(), + this, + foreignKeyDescriptor.getTargetPart(), + creationProcess + ); + } + else { + this.circularFetchModelPart = null; + } + } + public String getIdentifyingColumnsTableExpression() { return identifyingColumnsTableExpression; } @@ -1051,34 +1076,6 @@ class Mother { We have a circularity but it is not bidirectional */ - final TableGroup parentTableGroup = creationState - .getSqlAstCreationState() - .getFromClauseAccess() - .getTableGroup( fetchParent.getNavigablePath() ); - final DomainResult foreignKeyDomainResult; - assert !creationState.isResolvingCircularFetch(); - try { - creationState.setResolvingCircularFetch( true ); - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - foreignKeyDomainResult = foreignKeyDescriptor.createKeyDomainResult( - fetchablePath, - createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), - fetchParent, - creationState - ); - } - else { - foreignKeyDomainResult = foreignKeyDescriptor.createTargetDomainResult( - fetchablePath, - parentTableGroup, - fetchParent, - creationState - ); - } - } - finally { - creationState.setResolvingCircularFetch( false ); - } return new CircularFetchImpl( this, fetchTiming, @@ -1086,13 +1083,52 @@ class Mother { fetchParent, isSelectByUniqueKey( sideNature ), parentNavigablePath, - foreignKeyDomainResult, + determineCircularKeyResult( fetchParent, fetchablePath, creationState ), creationState ); } return null; } + private DomainResult determineCircularKeyResult( + FetchParent fetchParent, + NavigablePath fetchablePath, + DomainResultCreationState creationState) { + final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); + final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() ); + assert !creationState.isResolvingCircularFetch(); + try { + creationState.setResolvingCircularFetch( true ); + if ( circularFetchModelPart != null ) { + return circularFetchModelPart.createDomainResult( + fetchablePath, + createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + null, + creationState + ); + } + else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { + return foreignKeyDescriptor.createKeyDomainResult( + fetchablePath, + createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + fetchParent, + creationState + ); + } + else { + return foreignKeyDescriptor.createTargetDomainResult( + fetchablePath, + parentTableGroup, + fetchParent, + creationState + ); + } + } + finally { + creationState.setResolvingCircularFetch( false ); + } + } + protected boolean isBidirectionalAttributeName( NavigablePath parentNavigablePath, ModelPart parentModelPart, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java new file mode 100644 index 000000000000..1c0b21f53f0d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java @@ -0,0 +1,119 @@ +/* + * 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.annotations.cid; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.Hibernate; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +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 = { + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class, + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19687") +@BytecodeEnhanced +public class EmbeddedIdLazyOneToOneCriteriaQueryTest { + + @Test + public void query(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = builder.createQuery( EntityA.class ); + final Root root = criteriaQuery.from( EntityA.class ); + criteriaQuery.where( root.get( "id" ).in( 1 ) ); + criteriaQuery.select( root ); + + final List entities = session.createQuery( criteriaQuery ).getResultList(); + assertThat( entities ).hasSize( 1 ); + assertThat( Hibernate.isPropertyInitialized( entities.get( 0 ), "entityB" ) ).isFalse(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA entityA = new EntityA( 1 ); + session.persist( entityA ); + final EntityB entityB = new EntityB( new EntityBId( entityA ) ); + session.persist( entityB ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.getSessionFactory().getSchemaManager().truncateMappedObjects() ); + } + + @Entity(name = "EntityA") + static class EntityA { + + @Id + private Integer id; + + @OneToOne(mappedBy = "id.entityA", fetch = FetchType.LAZY) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(Integer id) { + this.id = id; + } + + } + + @Entity(name = "EntityB") + static class EntityB { + + @EmbeddedId + private EntityBId id; + + public EntityB() { + } + + public EntityB(EntityBId id) { + this.id = id; + } + + } + + @Embeddable + static class EntityBId { + + @OneToOne(fetch = FetchType.LAZY) + private EntityA entityA; + + public EntityBId() { + } + + public EntityBId(EntityA entityA) { + this.entityA = entityA; + } + + } + +}