diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index 89b4a48424..7a3e40b8b3 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -362,7 +362,6 @@ public class ToOneAttributeMapping isInternalLoadNullable = isNullable(); } - if ( referencedPropertyName == null ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); targetKeyPropertyNames.add( EntityIdentifierMapping.ROLE_LOCAL_NAME ); @@ -380,18 +379,12 @@ public class ToOneAttributeMapping if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; - addPrefixedPropertyNames( + addPrefixedPropertyPaths( targetKeyPropertyNames, targetKeyPropertyName, compositeType.getSubtypes()[0], declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( - targetKeyPropertyNames, - ForeignKeyDescriptor.PART_NAME, - compositeType.getSubtypes()[0], - declaringEntityPersister.getFactory() - ); } else { this.targetKeyPropertyName = EntityIdentifierMapping.ROLE_LOCAL_NAME; @@ -401,52 +394,34 @@ public class ToOneAttributeMapping propertyType, declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( + addPrefixedPropertyPaths( targetKeyPropertyNames, targetKeyPropertyName, propertyType, declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( - targetKeyPropertyNames, - ForeignKeyDescriptor.PART_NAME, - propertyType, - declaringEntityPersister.getFactory() - ); } } else { this.targetKeyPropertyName = entityBinding.getIdentifierProperty().getName(); - addPrefixedPropertyNames( + addPrefixedPropertyPaths( targetKeyPropertyNames, targetKeyPropertyName, propertyType, declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( - targetKeyPropertyNames, - ForeignKeyDescriptor.PART_NAME, - propertyType, - declaringEntityPersister.getFactory() - ); } this.targetKeyPropertyNames = targetKeyPropertyNames; } else if ( bootValue.isReferenceToPrimaryKey() ) { this.targetKeyPropertyName = referencedPropertyName; final Set targetKeyPropertyNames = new HashSet<>( 2 ); - addPrefixedPropertyNames( + addPrefixedPropertyPaths( targetKeyPropertyNames, targetKeyPropertyName, bootValue.getType(), declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( - targetKeyPropertyNames, - ForeignKeyDescriptor.PART_NAME, - bootValue.getType(), - declaringEntityPersister.getFactory() - ); this.targetKeyPropertyNames = targetKeyPropertyNames; } else { @@ -458,18 +433,12 @@ public class ToOneAttributeMapping && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; - addPrefixedPropertyNames( + addPrefixedPropertyPaths( targetKeyPropertyNames, targetKeyPropertyName, compositeType.getSubtypes()[0], declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( - targetKeyPropertyNames, - ForeignKeyDescriptor.PART_NAME, - compositeType.getSubtypes()[0], - declaringEntityPersister.getFactory() - ); this.targetKeyPropertyNames = targetKeyPropertyNames; } else { @@ -480,18 +449,12 @@ public class ToOneAttributeMapping if ( ( mapsIdAttributeName = findMapsIdPropertyName( entityMappingType, referencedPropertyName ) ) != null ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); targetKeyPropertyNames.add( targetKeyPropertyName ); - addPrefixedPropertyNames( + addPrefixedPropertyPaths( targetKeyPropertyNames, mapsIdAttributeName, entityMappingType.getEntityPersister().getIdentifierType(), declaringEntityPersister.getFactory() ); - addPrefixedPropertyNames( - targetKeyPropertyNames, - ForeignKeyDescriptor.PART_NAME, - entityMappingType.getEntityPersister().getIdentifierType(), - declaringEntityPersister.getFactory() - ); this.targetKeyPropertyNames = targetKeyPropertyNames; } else { @@ -666,6 +629,31 @@ public class ToOneAttributeMapping return null; } + private static void addPrefixedPropertyPaths( + Set targetKeyPropertyNames, + String prefix, + Type type, + SessionFactoryImplementor factory) { + addPrefixedPropertyNames( + targetKeyPropertyNames, + prefix, + type, + factory + ); + addPrefixedPropertyNames( + targetKeyPropertyNames, + ForeignKeyDescriptor.PART_NAME, + type, + factory + ); + addPrefixedPropertyNames( + targetKeyPropertyNames, + EntityIdentifierMapping.ROLE_LOCAL_NAME, + type, + factory + ); + } + public static void addPrefixedPropertyNames( Set targetKeyPropertyNames, String prefix, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java index 609de7b8e6..f00206fc12 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java @@ -13,20 +13,25 @@ import java.util.function.Supplier; import org.hibernate.graph.spi.GraphHelper; import org.hibernate.metamodel.AttributeClassification; import org.hibernate.metamodel.internal.MetadataContext; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.AnyMappingDomainType; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SimpleDomainType; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.query.SemanticException; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.hql.spi.SqmCreationState; import org.hibernate.query.sqm.internal.SqmMappingModelHelper; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.spi.EntityIdentifierNavigablePath; +import org.hibernate.spi.NavigablePath; import org.hibernate.type.descriptor.java.JavaType; /** @@ -162,6 +167,20 @@ public class SingularAttributeImpl metadataContext ); } + + @Override + public NavigablePath createNavigablePath(SqmPath parent, String alias) { + if ( parent == null ) { + throw new IllegalArgumentException( + "`lhs` cannot be null for a sub-navigable reference - " + parent + ); + } + NavigablePath navigablePath = parent.getNavigablePath(); + if ( parent.getReferencedPathSource() instanceof PluralPersistentAttribute ) { + navigablePath = navigablePath.append( CollectionPart.Nature.ELEMENT.getName() ); + } + return new EntityIdentifierNavigablePath( navigablePath, SqmCreationHelper.determineAlias( alias ), getName() ); + } } /** diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmJoinable.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmJoinable.java index 2a46e4e4de..f9e8b6ab62 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmJoinable.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmJoinable.java @@ -7,10 +7,12 @@ package org.hibernate.query.sqm; import org.hibernate.query.hql.spi.SqmCreationState; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmJoinType; -import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; +import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.spi.NavigablePath; /** * Specialization for attributes that that can be used in creating SQM joins @@ -29,4 +31,8 @@ public interface SqmJoinable { SqmCreationState creationState); String getName(); + + default NavigablePath createNavigablePath(SqmPath parent, String alias) { + return SqmCreationHelper.buildSubNavigablePath( parent, getName(), alias ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java index ae3dadd085..238450d18b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java @@ -35,7 +35,7 @@ public class SqmCreationHelper { return lhs.append( base, determineAlias( alias ) ); } - private static String determineAlias(String alias) { + public static String determineAlias(String alias) { // Make sure we always create a unique alias, otherwise we might use a wrong table group for the same join if ( alias == null ) { return Long.toString( System.nanoTime() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index d2e60f8032..bff7b2130a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -272,6 +272,7 @@ import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.query.sqm.tree.update.SqmAssignment; import org.hibernate.query.sqm.tree.update.SqmSetClause; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; @@ -382,6 +383,7 @@ import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.FetchableContainer; +import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; import org.hibernate.sql.results.graph.instantiation.internal.DynamicInstantiation; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; import org.hibernate.sql.results.internal.SqlSelectionImpl; @@ -7162,7 +7164,16 @@ public abstract class BaseSqmToSqlAstConverter extends Base // .getOrMakeJavaDescriptor( namedClass ); } - private void addFetch(ImmutableFetchList.Builder fetches, FetchParent fetchParent, Fetchable fetchable, Boolean isKeyFetchable) { + @Override + public Fetch visitIdentifierFetch(EntityResultGraphNode fetchParent) { + final EntityIdentifierMapping identifierMapping = fetchParent.getEntityValuedModelPart() + .getEntityMappingType() + .getIdentifierMapping(); + final Fetchable fetchableIdentifierMapping = (Fetchable) identifierMapping; + return createFetch( fetchParent, fetchableIdentifierMapping, true ); + } + + private Fetch createFetch(FetchParent fetchParent, Fetchable fetchable, Boolean isKeyFetchable) { final NavigablePath resolvedNavigablePath = fetchParent.resolveNavigablePath( fetchable ); final Map.Entry> sqlSelectionsToTrack = trackedFetchSelectionsForGroup.get( resolvedNavigablePath ); final int sqlSelectionStartIndexForFetch; @@ -7308,8 +7319,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base ); if ( biDirectionalFetch != null ) { - fetches.add( biDirectionalFetch ); - return; + return biDirectionalFetch; } } final Fetch fetch = buildFetch( @@ -7344,8 +7354,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base currentBagRole = fetchable.getNavigableRole().getNavigableName(); } } - fetches.add( fetch ); } + return fetch; } finally { if ( incrementFetchDepth ) { @@ -7374,10 +7384,16 @@ public abstract class BaseSqmToSqlAstConverter extends Base final int size = referencedMappingContainer.getNumberOfFetchables(); final ImmutableFetchList.Builder fetches = new ImmutableFetchList.Builder( referencedMappingContainer ); for ( int i = 0; i < keySize; i++ ) { - addFetch( fetches, fetchParent, referencedMappingContainer.getKeyFetchable( i ), true ); + final Fetch fetch = createFetch( fetchParent, referencedMappingContainer.getKeyFetchable( i ), true ); + if ( fetch != null ) { + fetches.add( fetch ); + } } for ( int i = 0; i < size; i++ ) { - addFetch( fetches, fetchParent, referencedMappingContainer.getFetchable( i ), false ); + final Fetch fetch = createFetch( fetchParent, referencedMappingContainer.getFetchable( i ), false ); + if ( fetch != null ) { + fetches.add( fetch ); + } } return fetches.build(); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java index c1836b0227..706cae6d57 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java @@ -49,7 +49,7 @@ public abstract class AbstractSqmAttributeJoin NodeBuilder nodeBuilder) { this( lhs, - SqmCreationHelper.buildSubNavigablePath( lhs, joinedNavigable.getName(), alias ), + joinedNavigable.createNavigablePath( lhs, alias ), joinedNavigable, alias == SqmCreationHelper.IMPLICIT_ALIAS ? null : alias, joinType, diff --git a/hibernate-core/src/main/java/org/hibernate/spi/EntityIdentifierNavigablePath.java b/hibernate-core/src/main/java/org/hibernate/spi/EntityIdentifierNavigablePath.java index 4b4be36811..17cecb3632 100644 --- a/hibernate-core/src/main/java/org/hibernate/spi/EntityIdentifierNavigablePath.java +++ b/hibernate-core/src/main/java/org/hibernate/spi/EntityIdentifierNavigablePath.java @@ -24,6 +24,11 @@ public class EntityIdentifierNavigablePath extends NavigablePath { this.identifierAttributeName = identifierAttributeName; } + public EntityIdentifierNavigablePath(NavigablePath parent, String alias, String identifierAttributeName) { + super( parent, EntityIdentifierMapping.ROLE_LOCAL_NAME, alias ); + this.identifierAttributeName = identifierAttributeName; + } + public String getIdentifierAttributeName() { return identifierAttributeName; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/FetchParent.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/FetchParent.java index 7947c2d9fd..6e157d83ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/FetchParent.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/FetchParent.java @@ -10,6 +10,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.FetchTiming; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -36,7 +37,7 @@ public interface FetchParent extends DomainResultGraphNode { default NavigablePath resolveNavigablePath(Fetchable fetchable) { final String fetchableName = fetchable.getFetchableName(); - if ( NavigablePath.IDENTIFIER_MAPPER_PROPERTY.equals( fetchableName ) ) { + if ( NavigablePath.IDENTIFIER_MAPPER_PROPERTY.equals( fetchableName ) || fetchable instanceof EntityIdentifierMapping ) { return new EntityIdentifierNavigablePath( getNavigablePath(), fetchableName ); } else { @@ -53,8 +54,7 @@ public interface FetchParent extends DomainResultGraphNode { else { fetchParentType = fetchableEntityType; } - if ( fetchParentType != fetchableEntityType ) { - // todo (6.0): if the fetchParentType is a subtype of fetchableEntityType this shouldn't be necessary + if ( fetchParentType != null && !fetchParentType.isTypeOrSuperType( fetchableEntityType ) ) { return getNavigablePath().treatAs( fetchableEntityType.getEntityName() ) .append( fetchableName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityResultImpl.java index 8f07ff307b..79d9c6de3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityResultImpl.java @@ -47,7 +47,7 @@ public class EntityResultImpl extends AbstractEntityResultGraphNode implements E for ( TableGroupJoin tableGroupJoin : tableGroup.getTableGroupJoins() ) { final NavigablePath navigablePath = tableGroupJoin.getNavigablePath(); if ( tableGroupJoin.getJoinedGroup().isFetched() - && fetchable.getFetchableName().equals( navigablePath.getLocalName() ) + && fetchable.getNavigableRole().getLocalName().equals( navigablePath.getLocalName() ) && tableGroupJoin.getJoinedGroup().getModelPart() == fetchable ) { return navigablePath; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardEntityGraphTraversalStateImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardEntityGraphTraversalStateImpl.java index 875bffd917..c9992da180 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardEntityGraphTraversalStateImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardEntityGraphTraversalStateImpl.java @@ -17,6 +17,7 @@ import org.hibernate.graph.spi.GraphImplementor; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.graph.spi.SubGraphImplementor; import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.internal.EntityCollectionPart; import org.hibernate.sql.results.graph.EntityGraphTraversalState; @@ -46,6 +47,9 @@ public class StandardEntityGraphTraversalStateImpl implements EntityGraphTravers @Override public TraversalResult traverse(FetchParent fetchParent, Fetchable fetchable, boolean exploreKeySubgraph) { assert !(fetchable instanceof CollectionPart); + if ( fetchable instanceof NonAggregatedIdentifierMapping ) { + return new TraversalResult( currentGraphContext, FetchTiming.IMMEDIATE, true ); + } final GraphImplementor previousContextRoot = currentGraphContext; AttributeNodeImplementor attributeNode = null; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/cdi/FetchEmbeddedIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/cdi/FetchEmbeddedIdTest.java new file mode 100644 index 0000000000..824e4413fe --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/cdi/FetchEmbeddedIdTest.java @@ -0,0 +1,271 @@ +package org.hibernate.orm.test.jpa.cdi; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Fetch; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa( + annotatedClasses = { + FetchEmbeddedIdTest.User.class, + FetchEmbeddedIdTest.GroupType.class, + FetchEmbeddedIdTest.Group.class, + FetchEmbeddedIdTest.UserGroup.class + } +) +@TestForIssue( jiraKey = "HHH-15875") +public class FetchEmbeddedIdTest { + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + User user = new User( 1l, "user name" ); + + GroupType groupType = new GroupType( 1l, "group type" ); + Group group = new Group( 1l, "user group", groupType ); + + UserGroupId userGroupId = new UserGroupId( user, group ); + + UserGroup userGroup = new UserGroup( userGroupId, "value" ); + + entityManager.persist( groupType ); + entityManager.persist( group ); + entityManager.persist( user ); + entityManager.persist( userGroup ); + } + ); + + } + + @Test + public void testCriteriaFetch(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + CriteriaQuery query = criteriaBuilder.createQuery( UserGroup.class ); + + Root root = query.from( UserGroup.class ); + + Fetch userGroupFetch = root.fetch( "userGroupId" ); + userGroupFetch.fetch( "user" ); + userGroupFetch.fetch( "group" ).fetch( "groupType" ); + + List results = entityManager.createQuery( query ).getResultList(); + assertThat( results ).hasSize( 1 ); + + UserGroup userGroup = results.get( 0 ); + UserGroupId userGroupId = userGroup.getUserGroupId(); + Group group = userGroupId.getGroup(); + assertTrue( Hibernate.isInitialized( group ) ); + String name = group.getName(); + assertThat( name ).isEqualTo( "user group" ); + + User user = userGroupId.getUser(); + assertTrue( Hibernate.isInitialized( user ) ); + } + ); + } + + @Test + public void testHqlFetch(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + + List results = entityManager.createQuery( "select ug from UserGroup ug join fetch ug.userGroupId ugi join fetch ugi.group join fetch ugi.user" ).getResultList(); + assertThat( results ).hasSize( 1 ); + + UserGroup userGroup = results.get( 0 ); + UserGroupId userGroupId = userGroup.getUserGroupId(); + Group group = userGroupId.getGroup(); + assertTrue( Hibernate.isInitialized( group ) ); + String name = group.getName(); + assertThat( name ).isEqualTo( "user group" ); + + User user = userGroupId.getUser(); + assertTrue( Hibernate.isInitialized( user ) ); + } + ); + } + + @Entity(name = "UserGroup") + public static class UserGroup { + + @EmbeddedId + private UserGroupId userGroupId; + + private String joinedPropertyValue; + + public UserGroup() { + } + + public UserGroup(UserGroupId userGroupId, String joinedPropertyValue) { + this.userGroupId = userGroupId; + this.joinedPropertyValue = joinedPropertyValue; + } + + public UserGroupId getUserGroupId() { + return userGroupId; + } + + public String getJoinedPropertyValue() { + return joinedPropertyValue; + } + } + + @Embeddable + public static class UserGroupId implements Serializable { + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private Group group; + + public UserGroupId() { + } + + public UserGroupId(User user, Group group) { + this.user = user; + this.group = group; + } + + public User getUser() { + return user; + } + + public Group getGroup() { + return group; + } + + @Override + public boolean equals(Object object) { + if ( this == object ) { + return true; + } + if ( !( object instanceof UserGroupId ) ) { + return false; + } + + UserGroupId that = (UserGroupId) object; + + return Objects.equals( user.getId(), that.user.getId() ) && Objects.equals( + group.getId(), + that.group.getId() + ); + } + + @Override + public int hashCode() { + return Objects.hash( user.getId(), group.getId() ); + } + } + + @Entity(name = "User") + @Table(name = "test_user") + public static class User { + + @Id + private Long id; + + private String name; + + public User() { + } + + public User(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + + @Entity(name = "GROUP") + @Table(name = "test_group") + public static class Group { + + @Id + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private GroupType groupType; + + public Group() { + } + + public Group(Long id, String name, GroupType groupType) { + this.id = id; + this.name = name; + this.groupType = groupType; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public GroupType getGroupType() { + return groupType; + } + } + + @Entity(name = "GroupType") + public static class GroupType { + + @Id + private Long id; + + private String name; + + public GroupType() { + + } + + public GroupType(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } +} \ No newline at end of file