Skip to content

Commit c9c6f46

Browse files
committed
HHH-19687 Correctly instantiate id for circular key-to-one fetch within embedded id
1 parent 28cf00f commit c9c6f46

File tree

4 files changed

+190
-31
lines changed

4 files changed

+190
-31
lines changed

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping original ) {
241241
creationProcess
242242
)
243243
);
244+
toOne.setupCircularFetchModelPart( creationProcess );
244245

245246
attributeMapping = toOne;
246247
currentIndex += attributeMapping.getJdbcTypeCount();

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,8 @@ public static boolean interpretToOneKeyDescriptor(
877877
return interpretNestedToOneKeyDescriptor(
878878
referencedEntityDescriptor,
879879
referencedPropertyName,
880-
attributeMapping
880+
attributeMapping,
881+
creationProcess
881882
);
882883
}
883884

@@ -909,6 +910,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa
909910
creationProcess
910911
);
911912
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
913+
attributeMapping.setupCircularFetchModelPart( creationProcess );
912914
}
913915
else {
914916
throw new UnsupportedOperationException(
@@ -997,6 +999,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa
997999
swapDirection
9981000
);
9991001
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
1002+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10001003
creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor );
10011004
}
10021005
else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) {
@@ -1013,6 +1016,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar
10131016
creationProcess
10141017
);
10151018
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
1019+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10161020
creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor );
10171021
}
10181022
else {
@@ -1033,12 +1037,14 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar
10331037
* @param referencedEntityDescriptor The entity which contains the inverse property
10341038
* @param referencedPropertyName The inverse property name path
10351039
* @param attributeMapping The attribute for which we try to set the foreign key
1040+
* @param creationProcess The creation process
10361041
* @return true if the foreign key is actually set
10371042
*/
10381043
private static boolean interpretNestedToOneKeyDescriptor(
10391044
EntityPersister referencedEntityDescriptor,
10401045
String referencedPropertyName,
1041-
ToOneAttributeMapping attributeMapping) {
1046+
ToOneAttributeMapping attributeMapping,
1047+
MappingModelCreationProcess creationProcess) {
10421048
final String[] propertyPath = split( ".", referencedPropertyName );
10431049
EmbeddableValuedModelPart lastEmbeddableModelPart = null;
10441050

@@ -1058,6 +1064,7 @@ else if ( modelPart instanceof ToOneAttributeMapping referencedAttributeMapping
10581064
}
10591065
else {
10601066
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
1067+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10611068
return true;
10621069
}
10631070
}

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.hibernate.metamodel.mapping.AttributeMapping;
3232
import org.hibernate.metamodel.mapping.AttributeMetadata;
3333
import org.hibernate.metamodel.mapping.CollectionPart;
34+
import org.hibernate.metamodel.mapping.CompositeIdentifierMapping;
3435
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
3536
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
3637
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
@@ -172,6 +173,7 @@ public class Entity1 {
172173
private ForeignKeyDescriptor.Nature sideNature;
173174
private String identifyingColumnsTableExpression;
174175
private boolean canUseParentTableGroup;
176+
private @Nullable EmbeddableValuedModelPart circularFetchModelPart;
175177

176178
/**
177179
* For Hibernate Reactive
@@ -841,6 +843,27 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) {
841843
&& declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression );
842844
}
843845

846+
public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) {
847+
if ( sideNature == ForeignKeyDescriptor.Nature.TARGET
848+
&& getAssociatedEntityMappingType().getIdentifierMapping() instanceof CompositeIdentifierMapping identifierMapping
849+
&& foreignKeyDescriptor.getKeyPart() != identifierMapping ) {
850+
// Setup a special embeddable model part for fetching the key object for a circular fetch.
851+
// This is needed if the association entity nests the "inverse" toOne association in the embedded id,
852+
// because then, the key part of the foreign key is just a simple value instead of the expected embedded id
853+
// when doing delayed creation/querying of target entities. See HHH-19687 for details
854+
this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart(
855+
identifierMapping,
856+
getDeclaringType(),
857+
this,
858+
foreignKeyDescriptor.getTargetPart(),
859+
creationProcess
860+
);
861+
}
862+
else {
863+
this.circularFetchModelPart = null;
864+
}
865+
}
866+
844867
public String getIdentifyingColumnsTableExpression() {
845868
return identifyingColumnsTableExpression;
846869
}
@@ -1024,48 +1047,59 @@ class Mother {
10241047
10251048
We have a circularity but it is not bidirectional
10261049
*/
1027-
final TableGroup parentTableGroup = creationState
1028-
.getSqlAstCreationState()
1029-
.getFromClauseAccess()
1030-
.getTableGroup( fetchParent.getNavigablePath() );
1031-
final DomainResult<?> foreignKeyDomainResult;
1032-
assert !creationState.isResolvingCircularFetch();
1033-
try {
1034-
creationState.setResolvingCircularFetch( true );
1035-
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
1036-
foreignKeyDomainResult = foreignKeyDescriptor.createKeyDomainResult(
1037-
fetchablePath,
1038-
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1039-
fetchParent,
1040-
creationState
1041-
);
1042-
}
1043-
else {
1044-
foreignKeyDomainResult = foreignKeyDescriptor.createTargetDomainResult(
1045-
fetchablePath,
1046-
parentTableGroup,
1047-
fetchParent,
1048-
creationState
1049-
);
1050-
}
1051-
}
1052-
finally {
1053-
creationState.setResolvingCircularFetch( false );
1054-
}
10551050
return new CircularFetchImpl(
10561051
this,
10571052
fetchTiming,
10581053
fetchablePath,
10591054
fetchParent,
10601055
isSelectByUniqueKey( sideNature ),
10611056
parentNavigablePath,
1062-
foreignKeyDomainResult,
1057+
determineCircularKeyResult( fetchParent, fetchablePath, creationState ),
10631058
creationState
10641059
);
10651060
}
10661061
return null;
10671062
}
10681063

1064+
private DomainResult<?> determineCircularKeyResult(
1065+
FetchParent fetchParent,
1066+
NavigablePath fetchablePath,
1067+
DomainResultCreationState creationState) {
1068+
final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess();
1069+
final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() );
1070+
assert !creationState.isResolvingCircularFetch();
1071+
try {
1072+
creationState.setResolvingCircularFetch( true );
1073+
if ( circularFetchModelPart != null ) {
1074+
return circularFetchModelPart.createDomainResult(
1075+
fetchablePath,
1076+
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1077+
null,
1078+
creationState
1079+
);
1080+
}
1081+
else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
1082+
return foreignKeyDescriptor.createKeyDomainResult(
1083+
fetchablePath,
1084+
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1085+
fetchParent,
1086+
creationState
1087+
);
1088+
}
1089+
else {
1090+
return foreignKeyDescriptor.createTargetDomainResult(
1091+
fetchablePath,
1092+
parentTableGroup,
1093+
fetchParent,
1094+
creationState
1095+
);
1096+
}
1097+
}
1098+
finally {
1099+
creationState.setResolvingCircularFetch( false );
1100+
}
1101+
}
1102+
10691103
protected boolean isBidirectionalAttributeName(
10701104
NavigablePath parentNavigablePath,
10711105
ModelPart parentModelPart,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.annotations.cid;
6+
7+
import jakarta.persistence.Embeddable;
8+
import jakarta.persistence.EmbeddedId;
9+
import jakarta.persistence.Entity;
10+
import jakarta.persistence.FetchType;
11+
import jakarta.persistence.Id;
12+
import jakarta.persistence.OneToOne;
13+
import jakarta.persistence.criteria.CriteriaBuilder;
14+
import jakarta.persistence.criteria.CriteriaQuery;
15+
import jakarta.persistence.criteria.Root;
16+
import org.hibernate.Hibernate;
17+
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
18+
import org.hibernate.testing.orm.junit.DomainModel;
19+
import org.hibernate.testing.orm.junit.Jira;
20+
import org.hibernate.testing.orm.junit.SessionFactory;
21+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
22+
import org.junit.jupiter.api.AfterAll;
23+
import org.junit.jupiter.api.BeforeAll;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.util.List;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
@DomainModel(annotatedClasses = {
31+
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class,
32+
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class,
33+
})
34+
@SessionFactory
35+
@Jira("https://hibernate.atlassian.net/browse/HHH-19687")
36+
@BytecodeEnhanced
37+
public class EmbeddedIdLazyOneToOneCriteriaQueryTest {
38+
39+
@Test
40+
public void query(SessionFactoryScope scope) {
41+
scope.inTransaction( session -> {
42+
final CriteriaBuilder builder = session.getCriteriaBuilder();
43+
final CriteriaQuery<EntityA> criteriaQuery = builder.createQuery( EntityA.class );
44+
final Root<EntityA> root = criteriaQuery.from( EntityA.class );
45+
criteriaQuery.where( root.get( "id" ).in( 1 ) );
46+
criteriaQuery.select( root );
47+
48+
final List<EntityA> entities = session.createQuery( criteriaQuery ).getResultList();
49+
assertThat( entities ).hasSize( 1 );
50+
assertThat( Hibernate.isPropertyInitialized( entities.get( 0 ), "entityB" ) ).isFalse();
51+
} );
52+
}
53+
54+
@BeforeAll
55+
public void setUp(SessionFactoryScope scope) {
56+
scope.inTransaction( session -> {
57+
final EntityA entityA = new EntityA( 1 );
58+
session.persist( entityA );
59+
final EntityB entityB = new EntityB( new EntityBId( entityA ) );
60+
session.persist( entityB );
61+
} );
62+
}
63+
64+
@AfterAll
65+
public void tearDown(SessionFactoryScope scope) {
66+
scope.inTransaction( session -> session.getSessionFactory().getSchemaManager().truncateMappedObjects() );
67+
}
68+
69+
@Entity(name = "EntityA")
70+
static class EntityA {
71+
72+
@Id
73+
private Integer id;
74+
75+
@OneToOne(mappedBy = "id.entityA", fetch = FetchType.LAZY)
76+
private EntityB entityB;
77+
78+
public EntityA() {
79+
}
80+
81+
public EntityA(Integer id) {
82+
this.id = id;
83+
}
84+
85+
}
86+
87+
@Entity(name = "EntityB")
88+
static class EntityB {
89+
90+
@EmbeddedId
91+
private EntityBId id;
92+
93+
public EntityB() {
94+
}
95+
96+
public EntityB(EntityBId id) {
97+
this.id = id;
98+
}
99+
100+
}
101+
102+
@Embeddable
103+
static class EntityBId {
104+
105+
@OneToOne(fetch = FetchType.LAZY)
106+
private EntityA entityA;
107+
108+
public EntityBId() {
109+
}
110+
111+
public EntityBId(EntityA entityA) {
112+
this.entityA = entityA;
113+
}
114+
115+
}
116+
117+
}

0 commit comments

Comments
 (0)