Skip to content

Commit 9fc7b40

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

File tree

5 files changed

+201
-42
lines changed

5 files changed

+201
-42
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
@@ -202,6 +202,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping ) {
202202
creationProcess
203203
)
204204
);
205+
toOne.setupCircularFetchModelPart( creationProcess );
205206

206207
attributeMapping = toOne;
207208
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
@@ -865,7 +865,8 @@ public static boolean interpretToOneKeyDescriptor(
865865
return interpretNestedToOneKeyDescriptor(
866866
referencedEntityDescriptor,
867867
referencedPropertyName,
868-
attributeMapping
868+
attributeMapping,
869+
creationProcess
869870
);
870871
}
871872

@@ -893,6 +894,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart ) {
893894
creationProcess
894895
);
895896
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
897+
attributeMapping.setupCircularFetchModelPart( creationProcess );
896898
}
897899
else if ( modelPart == null ) {
898900
throw new IllegalArgumentException( "Unable to find attribute " + bootProperty.getPersistentClass()
@@ -985,6 +987,7 @@ else if ( modelPart == null ) {
985987
swapDirection
986988
);
987989
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
990+
attributeMapping.setupCircularFetchModelPart( creationProcess );
988991
creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor );
989992
}
990993
else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
@@ -1001,6 +1004,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
10011004
creationProcess
10021005
);
10031006
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
1007+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10041008
creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor );
10051009
}
10061010
else {
@@ -1021,13 +1025,15 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
10211025
* @param referencedEntityDescriptor The entity which contains the inverse property
10221026
* @param referencedPropertyName The inverse property name path
10231027
* @param attributeMapping The attribute for which we try to set the foreign key
1028+
* @param creationProcess The creation process
10241029
* @return true if the foreign key is actually set
10251030
*/
10261031
private static boolean interpretNestedToOneKeyDescriptor(
10271032
EntityPersister referencedEntityDescriptor,
10281033
String referencedPropertyName,
1029-
ToOneAttributeMapping attributeMapping) {
1030-
String[] propertyPath = StringHelper.split( ".", referencedPropertyName );
1034+
ToOneAttributeMapping attributeMapping,
1035+
MappingModelCreationProcess creationProcess) {
1036+
final String[] propertyPath = StringHelper.split( ".", referencedPropertyName );
10311037
EmbeddableValuedModelPart lastEmbeddableModelPart = null;
10321038

10331039
for ( int i = 0; i < propertyPath.length; i++ ) {
@@ -1052,6 +1058,7 @@ private static boolean interpretNestedToOneKeyDescriptor(
10521058
}
10531059

10541060
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
1061+
attributeMapping.setupCircularFetchModelPart( creationProcess );
10551062
return true;
10561063
}
10571064
if ( modelPart instanceof EmbeddableValuedModelPart ) {

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

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.hibernate.metamodel.mapping.AttributeMapping;
4040
import org.hibernate.metamodel.mapping.AttributeMetadata;
4141
import org.hibernate.metamodel.mapping.CollectionPart;
42+
import org.hibernate.metamodel.mapping.CompositeIdentifierMapping;
4243
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
4344
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
4445
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
@@ -161,6 +162,7 @@ public class Entity1 {
161162
private ForeignKeyDescriptor.Nature sideNature;
162163
private String identifyingColumnsTableExpression;
163164
private boolean canUseParentTableGroup;
165+
private EmbeddableValuedModelPart circularFetchModelPart;
164166

165167
/**
166168
* For Hibernate Reactive
@@ -832,6 +834,29 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) {
832834
&& declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression );
833835
}
834836

837+
public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) {
838+
final EntityIdentifierMapping entityIdentifierMapping = getAssociatedEntityMappingType().getIdentifierMapping();
839+
if ( sideNature == ForeignKeyDescriptor.Nature.TARGET
840+
&& entityIdentifierMapping instanceof CompositeIdentifierMapping
841+
&& foreignKeyDescriptor.getKeyPart() != entityIdentifierMapping ) {
842+
// Setup a special embeddable model part for fetching the key object for a circular fetch.
843+
// This is needed if the association entity nests the "inverse" toOne association in the embedded id,
844+
// because then, the key part of the foreign key is just a simple value instead of the expected embedded id
845+
// when doing delayed creation/querying of target entities. See HHH-19687 for details
846+
final CompositeIdentifierMapping identifierMapping = (CompositeIdentifierMapping) entityIdentifierMapping;
847+
this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart(
848+
identifierMapping,
849+
getDeclaringType(),
850+
this,
851+
foreignKeyDescriptor.getTargetPart(),
852+
creationProcess
853+
);
854+
}
855+
else {
856+
this.circularFetchModelPart = null;
857+
}
858+
}
859+
835860
public String getIdentifyingColumnsTableExpression() {
836861
return identifyingColumnsTableExpression;
837862
}
@@ -1012,34 +1037,6 @@ class Mother {
10121037
10131038
We have a circularity but it is not bidirectional
10141039
*/
1015-
final TableGroup parentTableGroup = creationState
1016-
.getSqlAstCreationState()
1017-
.getFromClauseAccess()
1018-
.getTableGroup( fetchParent.getNavigablePath() );
1019-
final DomainResult<?> foreignKeyDomainResult;
1020-
assert !creationState.isResolvingCircularFetch();
1021-
try {
1022-
creationState.setResolvingCircularFetch( true );
1023-
if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
1024-
foreignKeyDomainResult = foreignKeyDescriptor.createKeyDomainResult(
1025-
fetchablePath,
1026-
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1027-
fetchParent,
1028-
creationState
1029-
);
1030-
}
1031-
else {
1032-
foreignKeyDomainResult = foreignKeyDescriptor.createTargetDomainResult(
1033-
fetchablePath,
1034-
parentTableGroup,
1035-
fetchParent,
1036-
creationState
1037-
);
1038-
}
1039-
}
1040-
finally {
1041-
creationState.setResolvingCircularFetch( false );
1042-
}
10431040
return new CircularFetchImpl(
10441041
this,
10451042
getEntityMappingType(),
@@ -1048,13 +1045,52 @@ class Mother {
10481045
fetchParent,
10491046
this,
10501047
isSelectByUniqueKey( sideNature ),
1051-
fetchablePath,
1052-
foreignKeyDomainResult
1048+
parentNavigablePath,
1049+
determineCircularKeyResult( fetchParent, fetchablePath, creationState )
10531050
);
10541051
}
10551052
return null;
10561053
}
10571054

1055+
private DomainResult<?> determineCircularKeyResult(
1056+
FetchParent fetchParent,
1057+
NavigablePath fetchablePath,
1058+
DomainResultCreationState creationState) {
1059+
final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess();
1060+
final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() );
1061+
assert !creationState.isResolvingCircularFetch();
1062+
try {
1063+
creationState.setResolvingCircularFetch( true );
1064+
if ( circularFetchModelPart != null ) {
1065+
return circularFetchModelPart.createDomainResult(
1066+
fetchablePath,
1067+
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1068+
null,
1069+
creationState
1070+
);
1071+
}
1072+
else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
1073+
return foreignKeyDescriptor.createKeyDomainResult(
1074+
fetchablePath,
1075+
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ),
1076+
fetchParent,
1077+
creationState
1078+
);
1079+
}
1080+
else {
1081+
return foreignKeyDescriptor.createTargetDomainResult(
1082+
fetchablePath,
1083+
parentTableGroup,
1084+
fetchParent,
1085+
creationState
1086+
);
1087+
}
1088+
}
1089+
finally {
1090+
creationState.setResolvingCircularFetch( false );
1091+
}
1092+
}
1093+
10581094
protected boolean isBidirectionalAttributeName(
10591095
NavigablePath parentNavigablePath,
10601096
ModelPart parentModelPart,

hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.hibernate.sql.results.graph.embeddable.internal;
88

9+
import org.hibernate.internal.util.NullnessUtil;
910
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
1011
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
1112
import org.hibernate.spi.NavigablePath;
@@ -32,33 +33,31 @@
3233
public class EmbeddableResultImpl<T> extends AbstractFetchParent implements EmbeddableResultGraphNode, DomainResult<T>, EmbeddableResult<T> {
3334
private final String resultVariable;
3435
private final boolean containsAnyNonScalars;
35-
private final NavigablePath initializerNavigablePath;
3636
private final EmbeddableMappingType fetchContainer;
3737

3838
public EmbeddableResultImpl(
3939
NavigablePath navigablePath,
4040
EmbeddableValuedModelPart modelPart,
4141
String resultVariable,
4242
DomainResultCreationState creationState) {
43-
super( navigablePath );
44-
this.fetchContainer = modelPart.getEmbeddableTypeDescriptor();
45-
this.resultVariable = resultVariable;
4643
/*
4744
An `{embeddable_result}` sub-path is created for the corresponding initializer to differentiate it from a fetch-initializer if this embedded is also fetched.
4845
The Jakarta Persistence spec says that any embedded value selected in the result should not be part of the state of any managed entity.
4946
Using this `{embeddable_result}` sub-path avoids this situation.
5047
*/
51-
this.initializerNavigablePath = navigablePath.append( "{embeddable_result}" );
48+
super( navigablePath.append( "{embeddable_result}" ) );
49+
this.fetchContainer = modelPart.getEmbeddableTypeDescriptor();
50+
this.resultVariable = resultVariable;
5251

5352
final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess();
5453

5554
fromClauseAccess.resolveTableGroup(
56-
navigablePath,
55+
getNavigablePath(),
5756
np -> {
5857
final EmbeddableValuedModelPart embeddedValueMapping = modelPart.getEmbeddableTypeDescriptor().getEmbeddedValueMapping();
59-
final TableGroup tableGroup = fromClauseAccess.findTableGroup( navigablePath.getParent() );
58+
final TableGroup tableGroup = fromClauseAccess.findTableGroup( NullnessUtil.castNonNull( np.getParent() ).getParent() );
6059
final TableGroupJoin tableGroupJoin = embeddedValueMapping.createTableGroupJoin(
61-
navigablePath,
60+
np,
6261
tableGroup,
6362
resultVariable,
6463
null,
@@ -123,7 +122,7 @@ public DomainResultAssembler<T> createResultAssembler(
123122
FetchParentAccess parentAccess,
124123
AssemblerCreationState creationState) {
125124
final EmbeddableInitializer initializer = creationState.resolveInitializer(
126-
initializerNavigablePath,
125+
getNavigablePath(),
127126
getReferencedModePart(),
128127
() -> new EmbeddableResultInitializer(
129128
this,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.*;
10+
import jakarta.persistence.criteria.CriteriaBuilder;
11+
import jakarta.persistence.criteria.CriteriaQuery;
12+
import jakarta.persistence.criteria.Root;
13+
import org.hibernate.Hibernate;
14+
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
15+
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
16+
import org.hibernate.testing.orm.junit.Jira;
17+
import org.junit.After;
18+
import org.junit.Before;
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
22+
import java.util.List;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
@Jira("https://hibernate.atlassian.net/browse/HHH-19687")
27+
@RunWith( BytecodeEnhancerRunner.class )
28+
public class EmbeddedIdLazyOneToOneCriteriaQueryTest extends BaseCoreFunctionalTestCase {
29+
30+
@Override
31+
protected Class<?>[] getAnnotatedClasses() {
32+
return new Class[]{
33+
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class,
34+
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class
35+
};
36+
}
37+
38+
@Test
39+
public void query() {
40+
inTransaction( session -> {
41+
final CriteriaBuilder builder = session.getCriteriaBuilder();
42+
final CriteriaQuery<EntityA> criteriaQuery = builder.createQuery( EntityA.class );
43+
final Root<EntityA> root = criteriaQuery.from( EntityA.class );
44+
criteriaQuery.where( root.get( "id" ).in( 1 ) );
45+
criteriaQuery.select( root );
46+
47+
final List<EntityA> entities = session.createQuery( criteriaQuery ).getResultList();
48+
assertThat( entities ).hasSize( 1 );
49+
assertThat( Hibernate.isPropertyInitialized( entities.get( 0 ), "entityB" ) ).isFalse();
50+
} );
51+
}
52+
53+
@Before
54+
public void setUp() {
55+
inTransaction( session -> {
56+
final EntityA entityA = new EntityA( 1 );
57+
session.persist( entityA );
58+
final EntityB entityB = new EntityB( new EntityBId( entityA ) );
59+
session.persist( entityB );
60+
} );
61+
}
62+
63+
@After
64+
public void tearDown() {
65+
inTransaction( session -> session.getSessionFactory().getSchemaManager().truncateMappedObjects() );
66+
}
67+
68+
@Entity(name = "EntityA")
69+
static class EntityA {
70+
71+
@Id
72+
private Integer id;
73+
74+
@OneToOne(mappedBy = "id.entityA", fetch = FetchType.LAZY)
75+
private EntityB entityB;
76+
77+
public EntityA() {
78+
}
79+
80+
public EntityA(Integer id) {
81+
this.id = id;
82+
}
83+
84+
}
85+
86+
@Entity(name = "EntityB")
87+
static class EntityB {
88+
89+
@EmbeddedId
90+
private EntityBId id;
91+
92+
public EntityB() {
93+
}
94+
95+
public EntityB(EntityBId id) {
96+
this.id = id;
97+
}
98+
99+
}
100+
101+
@Embeddable
102+
static class EntityBId {
103+
104+
@OneToOne(fetch = FetchType.LAZY)
105+
private EntityA entityA;
106+
107+
public EntityBId() {
108+
}
109+
110+
public EntityBId(EntityA entityA) {
111+
this.entityA = entityA;
112+
}
113+
114+
}
115+
116+
}

0 commit comments

Comments
 (0)