Skip to content

Commit d9f2b2f

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

File tree

4 files changed

+195
-32
lines changed

4 files changed

+195
-32
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
@@ -245,6 +245,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping ) {
245245
creationProcess
246246
)
247247
);
248+
toOne.setupCircularFetchModelPart( creationProcess );
248249

249250
attributeMapping = toOne;
250251
currentIndex += attributeMapping.getJdbcTypeCount();

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,8 @@ public static boolean interpretToOneKeyDescriptor(
893893
return interpretNestedToOneKeyDescriptor(
894894
referencedEntityDescriptor,
895895
referencedPropertyName,
896-
attributeMapping
896+
attributeMapping,
897+
creationProcess
897898
);
898899
}
899900

@@ -921,6 +922,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart ) {
921922
creationProcess
922923
);
923924
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
925+
attributeMapping.setupCircularFetchModelPart( creationProcess );
924926
}
925927
else if ( modelPart == null ) {
926928
throw new IllegalArgumentException( "Unable to find attribute " + bootProperty.getPersistentClass()
@@ -1017,6 +1019,7 @@ else if ( modelPart == null ) {
10171019
swapDirection
10181020
);
10191021
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
1022+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10201023
creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor );
10211024
}
10221025
else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
@@ -1033,6 +1036,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
10331036
creationProcess
10341037
);
10351038
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
1039+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10361040
creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor );
10371041
}
10381042
else {
@@ -1053,13 +1057,15 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
10531057
* @param referencedEntityDescriptor The entity which contains the inverse property
10541058
* @param referencedPropertyName The inverse property name path
10551059
* @param attributeMapping The attribute for which we try to set the foreign key
1060+
* @param creationProcess The creation process
10561061
* @return true if the foreign key is actually set
10571062
*/
10581063
private static boolean interpretNestedToOneKeyDescriptor(
10591064
EntityPersister referencedEntityDescriptor,
10601065
String referencedPropertyName,
1061-
ToOneAttributeMapping attributeMapping) {
1062-
String[] propertyPath = StringHelper.split( ".", referencedPropertyName );
1066+
ToOneAttributeMapping attributeMapping,
1067+
MappingModelCreationProcess creationProcess) {
1068+
final String[] propertyPath = StringHelper.split( ".", referencedPropertyName );
10631069
EmbeddableValuedModelPart lastEmbeddableModelPart = null;
10641070

10651071
for ( int i = 0; i < propertyPath.length; i++ ) {
@@ -1084,6 +1090,7 @@ private static boolean interpretNestedToOneKeyDescriptor(
10841090
}
10851091

10861092
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
1093+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10871094
return true;
10881095
}
10891096
if ( modelPart instanceof EmbeddableValuedModelPart ) {

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

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.hibernate.metamodel.mapping.AttributeMapping;
4242
import org.hibernate.metamodel.mapping.AttributeMetadata;
4343
import org.hibernate.metamodel.mapping.CollectionPart;
44+
import org.hibernate.metamodel.mapping.CompositeIdentifierMapping;
4445
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
4546
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
4647
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
@@ -178,6 +179,7 @@ public class Entity1 {
178179
private ForeignKeyDescriptor.Nature sideNature;
179180
private String identifyingColumnsTableExpression;
180181
private boolean canUseParentTableGroup;
182+
private @Nullable EmbeddableValuedModelPart circularFetchModelPart;
181183

182184
/**
183185
* For Hibernate Reactive
@@ -868,6 +870,29 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) {
868870
&& declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression );
869871
}
870872

873+
public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) {
874+
final EntityIdentifierMapping entityIdentifierMapping = getAssociatedEntityMappingType().getIdentifierMapping();
875+
if ( sideNature == ForeignKeyDescriptor.Nature.TARGET
876+
&& entityIdentifierMapping instanceof CompositeIdentifierMapping
877+
&& foreignKeyDescriptor.getKeyPart() != entityIdentifierMapping ) {
878+
// Setup a special embeddable model part for fetching the key object for a circular fetch.
879+
// This is needed if the association entity nests the "inverse" toOne association in the embedded id,
880+
// because then, the key part of the foreign key is just a simple value instead of the expected embedded id
881+
// when doing delayed creation/querying of target entities. See HHH-19687 for details
882+
final CompositeIdentifierMapping identifierMapping = (CompositeIdentifierMapping) entityIdentifierMapping;
883+
this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart(
884+
identifierMapping,
885+
getDeclaringType(),
886+
this,
887+
foreignKeyDescriptor.getTargetPart(),
888+
creationProcess
889+
);
890+
}
891+
else {
892+
this.circularFetchModelPart = null;
893+
}
894+
}
895+
871896
public String getIdentifyingColumnsTableExpression() {
872897
return identifyingColumnsTableExpression;
873898
}
@@ -1051,48 +1076,59 @@ class Mother {
10511076
10521077
We have a circularity but it is not bidirectional
10531078
*/
1054-
final TableGroup parentTableGroup = creationState
1055-
.getSqlAstCreationState()
1056-
.getFromClauseAccess()
1057-
.getTableGroup( fetchParent.getNavigablePath() );
1058-
final DomainResult<?> foreignKeyDomainResult;
1059-
assert !creationState.isResolvingCircularFetch();
1060-
try {
1061-
creationState.setResolvingCircularFetch( true );
1062-
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
1063-
foreignKeyDomainResult = foreignKeyDescriptor.createKeyDomainResult(
1064-
fetchablePath,
1065-
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1066-
fetchParent,
1067-
creationState
1068-
);
1069-
}
1070-
else {
1071-
foreignKeyDomainResult = foreignKeyDescriptor.createTargetDomainResult(
1072-
fetchablePath,
1073-
parentTableGroup,
1074-
fetchParent,
1075-
creationState
1076-
);
1077-
}
1078-
}
1079-
finally {
1080-
creationState.setResolvingCircularFetch( false );
1081-
}
10821079
return new CircularFetchImpl(
10831080
this,
10841081
fetchTiming,
10851082
fetchablePath,
10861083
fetchParent,
10871084
isSelectByUniqueKey( sideNature ),
10881085
parentNavigablePath,
1089-
foreignKeyDomainResult,
1086+
determineCircularKeyResult( fetchParent, fetchablePath, creationState ),
10901087
creationState
10911088
);
10921089
}
10931090
return null;
10941091
}
10951092

1093+
private DomainResult<?> determineCircularKeyResult(
1094+
FetchParent fetchParent,
1095+
NavigablePath fetchablePath,
1096+
DomainResultCreationState creationState) {
1097+
final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess();
1098+
final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() );
1099+
assert !creationState.isResolvingCircularFetch();
1100+
try {
1101+
creationState.setResolvingCircularFetch( true );
1102+
if ( circularFetchModelPart != null ) {
1103+
return circularFetchModelPart.createDomainResult(
1104+
fetchablePath,
1105+
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1106+
null,
1107+
creationState
1108+
);
1109+
}
1110+
else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
1111+
return foreignKeyDescriptor.createKeyDomainResult(
1112+
fetchablePath,
1113+
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1114+
fetchParent,
1115+
creationState
1116+
);
1117+
}
1118+
else {
1119+
return foreignKeyDescriptor.createTargetDomainResult(
1120+
fetchablePath,
1121+
parentTableGroup,
1122+
fetchParent,
1123+
creationState
1124+
);
1125+
}
1126+
}
1127+
finally {
1128+
creationState.setResolvingCircularFetch( false );
1129+
}
1130+
}
1131+
10961132
protected boolean isBidirectionalAttributeName(
10971133
NavigablePath parentNavigablePath,
10981134
ModelPart parentModelPart,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
5+
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
6+
*/
7+
package org.hibernate.orm.test.annotations.cid;
8+
9+
import jakarta.persistence.Embeddable;
10+
import jakarta.persistence.EmbeddedId;
11+
import jakarta.persistence.Entity;
12+
import jakarta.persistence.FetchType;
13+
import jakarta.persistence.Id;
14+
import jakarta.persistence.OneToOne;
15+
import jakarta.persistence.criteria.CriteriaBuilder;
16+
import jakarta.persistence.criteria.CriteriaQuery;
17+
import jakarta.persistence.criteria.Root;
18+
import org.hibernate.Hibernate;
19+
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
20+
import org.hibernate.testing.orm.junit.DomainModel;
21+
import org.hibernate.testing.orm.junit.Jira;
22+
import org.hibernate.testing.orm.junit.SessionFactory;
23+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
24+
import org.junit.jupiter.api.AfterAll;
25+
import org.junit.jupiter.api.BeforeAll;
26+
import org.junit.jupiter.api.Test;
27+
28+
import java.util.List;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
@DomainModel(annotatedClasses = {
33+
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class,
34+
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class,
35+
})
36+
@SessionFactory
37+
@Jira("https://hibernate.atlassian.net/browse/HHH-19687")
38+
@BytecodeEnhanced
39+
public class EmbeddedIdLazyOneToOneCriteriaQueryTest {
40+
41+
@Test
42+
public void query(SessionFactoryScope scope) {
43+
scope.inTransaction( session -> {
44+
final CriteriaBuilder builder = session.getCriteriaBuilder();
45+
final CriteriaQuery<EntityA> criteriaQuery = builder.createQuery( EntityA.class );
46+
final Root<EntityA> root = criteriaQuery.from( EntityA.class );
47+
criteriaQuery.where( root.get( "id" ).in( 1 ) );
48+
criteriaQuery.select( root );
49+
50+
final List<EntityA> entities = session.createQuery( criteriaQuery ).getResultList();
51+
assertThat( entities ).hasSize( 1 );
52+
assertThat( Hibernate.isPropertyInitialized( entities.get( 0 ), "entityB" ) ).isFalse();
53+
} );
54+
}
55+
56+
@BeforeAll
57+
public void setUp(SessionFactoryScope scope) {
58+
scope.inTransaction( session -> {
59+
final EntityA entityA = new EntityA( 1 );
60+
session.persist( entityA );
61+
final EntityB entityB = new EntityB( new EntityBId( entityA ) );
62+
session.persist( entityB );
63+
} );
64+
}
65+
66+
@AfterAll
67+
public void tearDown(SessionFactoryScope scope) {
68+
scope.inTransaction( session -> session.getSessionFactory().getSchemaManager().truncateMappedObjects() );
69+
}
70+
71+
@Entity(name = "EntityA")
72+
static class EntityA {
73+
74+
@Id
75+
private Integer id;
76+
77+
@OneToOne(mappedBy = "id.entityA", fetch = FetchType.LAZY)
78+
private EntityB entityB;
79+
80+
public EntityA() {
81+
}
82+
83+
public EntityA(Integer id) {
84+
this.id = id;
85+
}
86+
87+
}
88+
89+
@Entity(name = "EntityB")
90+
static class EntityB {
91+
92+
@EmbeddedId
93+
private EntityBId id;
94+
95+
public EntityB() {
96+
}
97+
98+
public EntityB(EntityBId id) {
99+
this.id = id;
100+
}
101+
102+
}
103+
104+
@Embeddable
105+
static class EntityBId {
106+
107+
@OneToOne(fetch = FetchType.LAZY)
108+
private EntityA entityA;
109+
110+
public EntityBId() {
111+
}
112+
113+
public EntityBId(EntityA entityA) {
114+
this.entityA = entityA;
115+
}
116+
117+
}
118+
119+
}

0 commit comments

Comments
 (0)