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 @@ -202,6 +202,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 @@ -865,7 +865,8 @@ public static boolean interpretToOneKeyDescriptor(
return interpretNestedToOneKeyDescriptor(
referencedEntityDescriptor,
referencedPropertyName,
attributeMapping
attributeMapping,
creationProcess
);
}

Expand Down Expand Up @@ -893,6 +894,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 @@ -985,6 +987,7 @@ else if ( modelPart == null ) {
swapDirection
);
attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor );
}
else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
Expand All @@ -1001,6 +1004,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) {
creationProcess
);
attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor );
attributeMapping.setupCircularFetchModelPart( creationProcess );
creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor );
}
else {
Expand All @@ -1021,13 +1025,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 @@ -1052,6 +1058,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 @@ -39,6 +39,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 @@ -161,6 +162,7 @@ public class Entity1 {
private ForeignKeyDescriptor.Nature sideNature;
private String identifyingColumnsTableExpression;
private boolean canUseParentTableGroup;
private EmbeddableValuedModelPart circularFetchModelPart;

/**
* For Hibernate Reactive
Expand Down Expand Up @@ -832,6 +834,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 @@ -1012,34 +1037,6 @@ 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,
getEntityMappingType(),
Expand All @@ -1048,13 +1045,52 @@ class Mother {
fetchParent,
this,
isSelectByUniqueKey( sideNature ),
fetchablePath,
foreignKeyDomainResult
parentNavigablePath,
determineCircularKeyResult( fetchParent, fetchablePath, 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
Expand Up @@ -6,6 +6,7 @@
*/
package org.hibernate.sql.results.graph.embeddable.internal;

import org.hibernate.internal.util.NullnessUtil;
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.spi.NavigablePath;
Expand All @@ -32,33 +33,31 @@
public class EmbeddableResultImpl<T> extends AbstractFetchParent implements EmbeddableResultGraphNode, DomainResult<T>, EmbeddableResult<T> {
private final String resultVariable;
private final boolean containsAnyNonScalars;
private final NavigablePath initializerNavigablePath;
private final EmbeddableMappingType fetchContainer;

public EmbeddableResultImpl(
NavigablePath navigablePath,
EmbeddableValuedModelPart modelPart,
String resultVariable,
DomainResultCreationState creationState) {
super( navigablePath );
this.fetchContainer = modelPart.getEmbeddableTypeDescriptor();
this.resultVariable = resultVariable;
/*
An `{embeddable_result}` sub-path is created for the corresponding initializer to differentiate it from a fetch-initializer if this embedded is also fetched.
The Jakarta Persistence spec says that any embedded value selected in the result should not be part of the state of any managed entity.
Using this `{embeddable_result}` sub-path avoids this situation.
*/
this.initializerNavigablePath = navigablePath.append( "{embeddable_result}" );
super( navigablePath.append( "{embeddable_result}" ) );
this.fetchContainer = modelPart.getEmbeddableTypeDescriptor();
this.resultVariable = resultVariable;

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

fromClauseAccess.resolveTableGroup(
navigablePath,
getNavigablePath(),
np -> {
final EmbeddableValuedModelPart embeddedValueMapping = modelPart.getEmbeddableTypeDescriptor().getEmbeddedValueMapping();
final TableGroup tableGroup = fromClauseAccess.findTableGroup( navigablePath.getParent() );
final TableGroup tableGroup = fromClauseAccess.findTableGroup( NullnessUtil.castNonNull( np.getParent() ).getParent() );
final TableGroupJoin tableGroupJoin = embeddedValueMapping.createTableGroupJoin(
navigablePath,
np,
tableGroup,
resultVariable,
null,
Expand Down Expand Up @@ -123,7 +122,7 @@ public DomainResultAssembler<T> createResultAssembler(
FetchParentAccess parentAccess,
AssemblerCreationState creationState) {
final EmbeddableInitializer initializer = creationState.resolveInitializer(
initializerNavigablePath,
getNavigablePath(),
getReferencedModePart(),
() -> new EmbeddableResultInitializer(
this,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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.*;
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.BytecodeEnhancerRunner;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.orm.junit.Jira;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@Jira("https://hibernate.atlassian.net/browse/HHH-19687")
@RunWith( BytecodeEnhancerRunner.class )
public class EmbeddedIdLazyOneToOneCriteriaQueryTest extends BaseCoreFunctionalTestCase {

@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[]{
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class,
EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class
};
}

@Test
public void query() {
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();
} );
}

@Before
public void setUp() {
inTransaction( session -> {
final EntityA entityA = new EntityA( 1 );
session.persist( entityA );
final EntityB entityB = new EntityB( new EntityBId( entityA ) );
session.persist( entityB );
} );
}

@After
public void tearDown() {
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