Skip to content
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 @@ -241,6 +241,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping original ) {
creationProcess
)
);
toOne.setupCircularFetchModelPart( creationProcess );

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

Expand Down Expand Up @@ -909,6 +910,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa
creationProcess
);
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
}
else {
throw new UnsupportedOperationException(
Expand Down Expand Up @@ -997,6 +999,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa
swapDirection
);
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor );
}
else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) {
Expand All @@ -1013,6 +1016,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar
creationProcess
);
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor );
}
else {
Expand All @@ -1033,12 +1037,14 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPar
* @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) {
ToOneAttributeMapping attributeMapping,
MappingModelCreationProcess creationProcess) {
final String[] propertyPath = split( ".", referencedPropertyName );
EmbeddableValuedModelPart lastEmbeddableModelPart = null;

Expand All @@ -1058,6 +1064,7 @@ else if ( modelPart instanceof ToOneAttributeMapping referencedAttributeMapping
}
else {
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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 @@ -172,6 +173,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 @@ -841,6 +843,27 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) {
&& declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression );
}

public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) {
if ( sideNature == ForeignKeyDescriptor.Nature.TARGET
&& getAssociatedEntityMappingType().getIdentifierMapping() instanceof CompositeIdentifierMapping identifierMapping
&& foreignKeyDescriptor.getKeyPart() != identifierMapping ) {
// 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
this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart(
identifierMapping,
getDeclaringType(),
this,
foreignKeyDescriptor.getTargetPart(),
creationProcess
);
}
else {
this.circularFetchModelPart = null;
}
}

public String getIdentifyingColumnsTableExpression() {
return identifyingColumnsTableExpression;
}
Expand Down Expand Up @@ -1024,48 +1047,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,117 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
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;
}

}

}