HHH-15875 Fix join fetch support for associations within embedded ids

This commit is contained in:
Christian Beikov 2023-01-11 12:50:57 +01:00
parent 2271e18ba5
commit a71e26e333
11 changed files with 365 additions and 56 deletions

View File

@ -362,7 +362,6 @@ public class ToOneAttributeMapping
isInternalLoadNullable = isNullable();
}
if ( referencedPropertyName == null ) {
final Set<String> 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<String> 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<String> 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<String> 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<String> 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<String> targetKeyPropertyNames,
String prefix,

View File

@ -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<D,J>
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() );
}
}
/**

View File

@ -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<O, E> {
SqmCreationState creationState);
String getName();
default NavigablePath createNavigablePath(SqmPath<?> parent, String alias) {
return SqmCreationHelper.buildSubNavigablePath( parent, getName(), alias );
}
}

View File

@ -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() );

View File

@ -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<T extends Statement> 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<Integer, List<SqlSelection>> sqlSelectionsToTrack = trackedFetchSelectionsForGroup.get( resolvedNavigablePath );
final int sqlSelectionStartIndexForFetch;
@ -7308,8 +7319,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
);
if ( biDirectionalFetch != null ) {
fetches.add( biDirectionalFetch );
return;
return biDirectionalFetch;
}
}
final Fetch fetch = buildFetch(
@ -7344,8 +7354,8 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
currentBagRole = fetchable.getNavigableRole().getNavigableName();
}
}
fetches.add( fetch );
}
return fetch;
}
finally {
if ( incrementFetchDepth ) {
@ -7374,10 +7384,16 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> 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();
}

View File

@ -49,7 +49,7 @@ public abstract class AbstractSqmAttributeJoin<O,T>
NodeBuilder nodeBuilder) {
this(
lhs,
SqmCreationHelper.buildSubNavigablePath( lhs, joinedNavigable.getName(), alias ),
joinedNavigable.createNavigablePath( lhs, alias ),
joinedNavigable,
alias == SqmCreationHelper.IMPLICIT_ALIAS ? null : alias,
joinType,

View File

@ -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;
}

View File

@ -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 );
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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<UserGroup> query = criteriaBuilder.createQuery( UserGroup.class );
Root<UserGroup> root = query.from( UserGroup.class );
Fetch<?, ?> userGroupFetch = root.fetch( "userGroupId" );
userGroupFetch.fetch( "user" );
userGroupFetch.fetch( "group" ).fetch( "groupType" );
List<UserGroup> 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<UserGroup> 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;
}
}
}