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 7c135eeb7712..aa9c012cf630 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 @@ -202,6 +202,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 d8c0fed987c4..12f4f272883e 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 @@ -865,7 +865,8 @@ public static boolean interpretToOneKeyDescriptor( return interpretNestedToOneKeyDescriptor( referencedEntityDescriptor, referencedPropertyName, - attributeMapping + attributeMapping, + creationProcess ); } @@ -893,6 +894,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() @@ -985,6 +987,7 @@ else if ( modelPart == null ) { swapDirection ); attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor ); } else if ( fkTarget instanceof EmbeddableValuedModelPart ) { @@ -1001,6 +1004,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) { creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor ); } else { @@ -1021,13 +1025,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++ ) { @@ -1052,6 +1058,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 26c600a3fbe1..30e2caa6764e 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 @@ -39,6 +39,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; @@ -161,6 +162,7 @@ public class Entity1 { private ForeignKeyDescriptor.Nature sideNature; private String identifyingColumnsTableExpression; private boolean canUseParentTableGroup; + private EmbeddableValuedModelPart circularFetchModelPart; /** * For Hibernate Reactive @@ -832,6 +834,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; } @@ -1012,34 +1037,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, getEntityMappingType(), @@ -1048,13 +1045,52 @@ class Mother { fetchParent, this, isSelectByUniqueKey( sideNature ), - fetchablePath, - foreignKeyDomainResult + parentNavigablePath, + determineCircularKeyResult( fetchParent, fetchablePath, 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/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java index 3c6070342549..67bc57323ae1 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.results.graph.embeddable.internal; +import org.hibernate.internal.util.NullnessUtil; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.spi.NavigablePath; @@ -32,7 +33,6 @@ public class EmbeddableResultImpl extends AbstractFetchParent implements EmbeddableResultGraphNode, DomainResult, EmbeddableResult { private final String resultVariable; private final boolean containsAnyNonScalars; - private final NavigablePath initializerNavigablePath; private final EmbeddableMappingType fetchContainer; public EmbeddableResultImpl( @@ -40,25 +40,24 @@ public EmbeddableResultImpl( EmbeddableValuedModelPart modelPart, String resultVariable, DomainResultCreationState creationState) { - super( navigablePath ); - this.fetchContainer = modelPart.getEmbeddableTypeDescriptor(); - this.resultVariable = resultVariable; /* An `{embeddable_result}` sub-path is created for the corresponding initializer to differentiate it from a fetch-initializer if this embedded is also fetched. The Jakarta Persistence spec says that any embedded value selected in the result should not be part of the state of any managed entity. Using this `{embeddable_result}` sub-path avoids this situation. */ - this.initializerNavigablePath = navigablePath.append( "{embeddable_result}" ); + super( navigablePath.append( "{embeddable_result}" ) ); + this.fetchContainer = modelPart.getEmbeddableTypeDescriptor(); + this.resultVariable = resultVariable; final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); fromClauseAccess.resolveTableGroup( - navigablePath, + getNavigablePath(), np -> { final EmbeddableValuedModelPart embeddedValueMapping = modelPart.getEmbeddableTypeDescriptor().getEmbeddedValueMapping(); - final TableGroup tableGroup = fromClauseAccess.findTableGroup( navigablePath.getParent() ); + final TableGroup tableGroup = fromClauseAccess.findTableGroup( NullnessUtil.castNonNull( np.getParent() ).getParent() ); final TableGroupJoin tableGroupJoin = embeddedValueMapping.createTableGroupJoin( - navigablePath, + np, tableGroup, resultVariable, null, @@ -123,7 +122,7 @@ public DomainResultAssembler createResultAssembler( FetchParentAccess parentAccess, AssemblerCreationState creationState) { final EmbeddableInitializer initializer = creationState.resolveInitializer( - initializerNavigablePath, + getNavigablePath(), getReferencedModePart(), () -> new EmbeddableResultInitializer( this, 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..3b2363175a7e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java @@ -0,0 +1,116 @@ +/* + * 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.*; +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.BytecodeEnhancerRunner; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.testing.orm.junit.Jira; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Jira("https://hibernate.atlassian.net/browse/HHH-19687") +@RunWith( BytecodeEnhancerRunner.class ) +public class EmbeddedIdLazyOneToOneCriteriaQueryTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class, + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class + }; + } + + @Test + public void query() { + 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(); + } ); + } + + @Before + public void setUp() { + inTransaction( session -> { + final EntityA entityA = new EntityA( 1 ); + session.persist( entityA ); + final EntityB entityB = new EntityB( new EntityBId( entityA ) ); + session.persist( entityB ); + } ); + } + + @After + public void tearDown() { + 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; + } + + } + +}