Skip to content

HHH-19687 Correctly instantiate id for circular key-to-one fetch with in embedded id #10791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping ) {
creationProcess
)
);
toOne.setupCircularFetchModelPart( creationProcess );

attributeMapping = toOne;
currentIndex += attributeMapping.getJdbcTypeCount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,8 @@ public static boolean interpretToOneKeyDescriptor(
return interpretNestedToOneKeyDescriptor(
referencedEntityDescriptor,
referencedPropertyName,
attributeMapping
attributeMapping,
creationProcess
);
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -1017,6 +1019,7 @@ else if ( modelPart == null ) {
swapDirection
);
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor );
}
else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
Expand All @@ -1033,6 +1036,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
creationProcess
);
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor );
}
else {
Expand All @@ -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++ ) {
Expand All @@ -1084,6 +1090,7 @@ private static boolean interpretNestedToOneKeyDescriptor(
}

attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
return true;
}
if ( modelPart instanceof EmbeddableValuedModelPart ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -178,6 +179,7 @@ public class Entity1 {
private ForeignKeyDescriptor.Nature sideNature;
private String identifyingColumnsTableExpression;
private boolean canUseParentTableGroup;
private @Nullable EmbeddableValuedModelPart circularFetchModelPart;

/**
* For Hibernate Reactive
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1051,48 +1076,59 @@ 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,
fetchablePath,
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<EntityA> criteriaQuery = builder.createQuery( EntityA.class );
final Root<EntityA> root = criteriaQuery.from( EntityA.class );
criteriaQuery.where( root.get( "id" ).in( 1 ) );
criteriaQuery.select( root );

final List<EntityA> 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;
}

}

}
Loading