From 966a56ee736174f0912a6d59aa6a18459c04bf36 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 | 11 +- .../internal/ToOneAttributeMapping.java | 92 +++++++++----- ...beddedIdLazyOneToOneCriteriaQueryTest.java | 117 ++++++++++++++++++ 4 files changed, 190 insertions(+), 31 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 112cbd25fc66..82bda15f6147 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 @@ -241,6 +241,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping original ) { 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 18862ede8e60..b20c539a4997 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 @@ -877,7 +877,8 @@ public static boolean interpretToOneKeyDescriptor( return interpretNestedToOneKeyDescriptor( referencedEntityDescriptor, referencedPropertyName, - attributeMapping + attributeMapping, + creationProcess ); } @@ -909,6 +910,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); } else { throw new UnsupportedOperationException( @@ -997,6 +999,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa swapDirection ); attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor ); } else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) { @@ -1013,6 +1016,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor ); } else { @@ -1033,12 +1037,14 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar * @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) { + ToOneAttributeMapping attributeMapping, + MappingModelCreationProcess creationProcess) { final String[] propertyPath = split( ".", referencedPropertyName ); EmbeddableValuedModelPart lastEmbeddableModelPart = null; @@ -1058,6 +1064,7 @@ else if ( modelPart instanceof ToOneAttributeMapping referencedAttributeMapping } else { attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); return true; } } 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 f62c7577e973..0b51e85a1e3e 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 @@ -31,6 +31,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; @@ -172,6 +173,7 @@ public class Entity1 { private ForeignKeyDescriptor.Nature sideNature; private String identifyingColumnsTableExpression; private boolean canUseParentTableGroup; + private @Nullable EmbeddableValuedModelPart circularFetchModelPart; /** * For Hibernate Reactive @@ -841,6 +843,27 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); } + public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) { + if ( sideNature == ForeignKeyDescriptor.Nature.TARGET + && getAssociatedEntityMappingType().getIdentifierMapping() instanceof CompositeIdentifierMapping identifierMapping + && foreignKeyDescriptor.getKeyPart() != identifierMapping ) { + // 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 + this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart( + identifierMapping, + getDeclaringType(), + this, + foreignKeyDescriptor.getTargetPart(), + creationProcess + ); + } + else { + this.circularFetchModelPart = null; + } + } + public String getIdentifyingColumnsTableExpression() { return identifyingColumnsTableExpression; } @@ -1024,34 +1047,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, @@ -1059,13 +1054,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..34ab1c467a9d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +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; + } + + } + +}