HHH-17837 Render target-side key for explicit plural joins when needed

Also, change how we determine whether we need to use the target-side to only the strictly needed cases (non-optimizable joins, `group by` or `order by` clauses)
This commit is contained in:
Marco Belladelli 2024-03-22 11:15:10 +01:00 committed by Marco Belladelli
parent b2f1725520
commit 9ba0dd7af0
11 changed files with 92 additions and 52 deletions

View File

@ -6,6 +6,8 @@
*/ */
package org.hibernate.metamodel.mapping; package org.hibernate.metamodel.mapping;
import java.util.Set;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
/** /**
@ -21,6 +23,8 @@ public interface EntityAssociationMapping extends ModelPart, Association, TableG
EntityMappingType getAssociatedEntityMappingType(); EntityMappingType getAssociatedEntityMappingType();
Set<String> getTargetKeyPropertyNames();
/** /**
* The model sub-part relative to the associated entity type that is the target * The model sub-part relative to the associated entity type that is the target
* of this association's foreign-key * of this association's foreign-key

View File

@ -60,7 +60,7 @@ public abstract class AbstractEntityCollectionPart implements EntityCollectionPa
private final EntityMappingType associatedEntityTypeDescriptor; private final EntityMappingType associatedEntityTypeDescriptor;
private final NotFoundAction notFoundAction; private final NotFoundAction notFoundAction;
private final Set<String> targetKeyPropertyNames; protected final Set<String> targetKeyPropertyNames;
public AbstractEntityCollectionPart( public AbstractEntityCollectionPart(
Nature nature, Nature nature,
@ -110,10 +110,6 @@ public abstract class AbstractEntityCollectionPart implements EntityCollectionPa
return getAssociatedEntityMappingType(); return getAssociatedEntityMappingType();
} }
protected Set<String> getTargetKeyPropertyNames() {
return targetKeyPropertyNames;
}
@Override @Override
public NavigableRole getNavigableRole() { public NavigableRole getNavigableRole() {
return navigableRole; return navigableRole;

View File

@ -7,6 +7,7 @@
package org.hibernate.metamodel.mapping.internal; package org.hibernate.metamodel.mapping.internal;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.NotFoundAction;
@ -135,6 +136,11 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple
return super.findSubPart( name, targetType ); return super.findSubPart( name, targetType );
} }
@Override
public Set<String> getTargetKeyPropertyNames() {
return targetKeyPropertyNames;
}
@Override @Override
public <X, Y> int breakDownJdbcValues( public <X, Y> int breakDownJdbcValues(
Object domainValue, Object domainValue,

View File

@ -899,6 +899,7 @@ public class ToOneAttributeMapping
return targetKeyPropertyName; return targetKeyPropertyName;
} }
@Override
public Set<String> getTargetKeyPropertyNames() { public Set<String> getTargetKeyPropertyNames() {
return targetKeyPropertyNames; return targetKeyPropertyNames;
} }

View File

@ -25,13 +25,14 @@ import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.BasicValuedMapping;
import org.hibernate.metamodel.mapping.Bindable; import org.hibernate.metamodel.mapping.Bindable;
import org.hibernate.metamodel.mapping.CollectionPart;
import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.ManagedMappingType;
import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.IllegalQueryOperationException;
@ -46,6 +47,7 @@ import org.hibernate.query.sqm.SqmPathSource;
import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.SqmQuerySource;
import org.hibernate.query.sqm.spi.JdbcParameterBySqmParameterAccess; import org.hibernate.query.sqm.spi.JdbcParameterBySqmParameterAccess;
import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmDmlStatement; import org.hibernate.query.sqm.tree.SqmDmlStatement;
import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmJoinType;
import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.SqmStatement;
@ -60,11 +62,13 @@ import org.hibernate.query.sqm.tree.from.SqmFrom;
import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmJoin;
import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin;
import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.select.SqmQueryPart;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelectableNode;
import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.query.sqm.tree.select.SqmSelection;
import org.hibernate.query.sqm.tree.select.SqmSortSpecification; import org.hibernate.query.sqm.tree.select.SqmSortSpecification;
import org.hibernate.spi.NavigablePath; import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.SqlTreeCreationException;
import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.JdbcParameter;
import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroup;
@ -79,6 +83,7 @@ import org.hibernate.type.internal.BasicTypeImpl;
import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.internal.ConvertedBasicTypeImpl;
import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.internal.util.NullnessUtil.castNonNull;
import static org.hibernate.query.sqm.tree.jpa.ParameterCollector.collectParameters; import static org.hibernate.query.sqm.tree.jpa.ParameterCollector.collectParameters;
/** /**
@ -132,13 +137,56 @@ public class SqmUtil {
} }
/** /**
* Utility that returns {@code true} if the specified {@link SqmPath sqmPath} should be * Utility that returns the entity association target's mapping type if the specified {@code sqmPath} should
* dereferenced using the target table mapping, i.e. when the path's lhs is an explicit join. * be dereferenced using the target table, i.e. when the path's lhs is an explicit join that is used in the
* group by clause, or defaults to the provided {@code modelPartContainer} otherwise.
*/ */
public static boolean needsTargetTableMapping(SqmPath<?> sqmPath, ModelPartContainer modelPartContainer) { public static ModelPartContainer getTargetMappingIfNeeded(
return modelPartContainer.getPartMappingType() != modelPartContainer SqmPath<?> sqmPath,
&& sqmPath.getLhs() instanceof SqmFrom<?, ?> ModelPartContainer modelPartContainer,
&& modelPartContainer.getPartMappingType() instanceof ManagedMappingType; SqmToSqlAstConverter sqlAstCreationState) {
final SqmQueryPart<?> queryPart = sqlAstCreationState.getCurrentSqmQueryPart();
if ( queryPart != null ) {
// We only need to do this for queries
final Clause clause = sqlAstCreationState.getCurrentClauseStack().getCurrent();
if ( clause != Clause.FROM && modelPartContainer.getPartMappingType() != modelPartContainer && sqmPath.getLhs() instanceof SqmFrom<?, ?> ) {
final ModelPart modelPart;
if ( modelPartContainer instanceof PluralAttributeMapping ) {
modelPart = getCollectionPart(
(PluralAttributeMapping) modelPartContainer,
castNonNull( sqmPath.getNavigablePath().getParent() )
);
}
else {
modelPart = modelPartContainer;
}
if ( modelPart instanceof EntityAssociationMapping ) {
final EntityAssociationMapping association = (EntityAssociationMapping) modelPart;
// If the path is one of the association's target key properties,
// we need to render the target side if in group/order by
if ( association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() )
&& ( clause == Clause.GROUP || clause == Clause.ORDER
|| !isFkOptimizationAllowed( sqmPath.getLhs() )
|| queryPart.getFirstQuerySpec().groupByClauseContains( sqmPath.getNavigablePath() ) ) ) {
return association.getAssociatedEntityMappingType();
}
}
}
}
return modelPartContainer;
}
private static CollectionPart getCollectionPart(PluralAttributeMapping attribute, NavigablePath path) {
final CollectionPart.Nature nature = CollectionPart.Nature.fromNameExact( path.getLocalName() );
if ( nature != null ) {
switch ( nature ) {
case ELEMENT:
return attribute.getElementDescriptor();
case INDEX:
return attribute.getIndexDescriptor();
}
}
return null;
} }
/** /**

View File

@ -13,7 +13,6 @@ import java.util.function.Consumer;
import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.ManagedMappingType;
import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.MappingType;
import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.ModelPartContainer;
@ -35,7 +34,7 @@ import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.update.Assignable; import org.hibernate.sql.ast.tree.update.Assignable;
import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.internal.util.NullnessUtil.castNonNull;
import static org.hibernate.query.sqm.internal.SqmUtil.needsTargetTableMapping; import static org.hibernate.query.sqm.internal.SqmUtil.getTargetMappingIfNeeded;
/** /**
* @author Steve Ebersole * @author Steve Ebersole
@ -82,20 +81,12 @@ public class BasicValuedPathInterpretation<T> extends AbstractSqmPathInterpretat
} }
} }
final ModelPart modelPart; // Use the target type to find the sub part if needed, otherwise just use the container
if ( needsTargetTableMapping( sqmPath, modelPartContainer ) ) { final ModelPart modelPart = getTargetMappingIfNeeded(
// We have to make sure we render the column of the target table sqmPath,
modelPart = ( (ManagedMappingType) modelPartContainer.getPartMappingType() ).findSubPart( modelPartContainer,
sqmPath.getReferencedPathSource().getPathName(), sqlAstCreationState
treatTarget ).findSubPart( sqmPath.getReferencedPathSource().getPathName(), treatTarget );
);
}
else {
modelPart = modelPartContainer.findSubPart(
sqmPath.getReferencedPathSource().getPathName(),
treatTarget
);
}
if ( modelPart == null ) { if ( modelPart == null ) {
if ( jpaQueryComplianceEnabled ) { if ( jpaQueryComplianceEnabled ) {

View File

@ -13,7 +13,6 @@ import java.util.function.Consumer;
import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.ManagedMappingType;
import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
@ -29,7 +28,7 @@ import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.update.Assignable; import org.hibernate.sql.ast.tree.update.Assignable;
import static org.hibernate.query.sqm.internal.SqmUtil.needsTargetTableMapping; import static org.hibernate.query.sqm.internal.SqmUtil.getTargetMappingIfNeeded;
/** /**
* @author Steve Ebersole * @author Steve Ebersole
@ -65,20 +64,12 @@ public class EmbeddableValuedPathInterpretation<T> extends AbstractSqmPathInterp
} }
final ModelPartContainer modelPartContainer = tableGroup.getModelPart(); final ModelPartContainer modelPartContainer = tableGroup.getModelPart();
final EmbeddableValuedModelPart mapping; // Use the target type to find the sub part if needed, otherwise just use the container
if ( needsTargetTableMapping( sqmPath, modelPartContainer ) ) { final EmbeddableValuedModelPart mapping = (EmbeddableValuedModelPart) getTargetMappingIfNeeded(
// We have to make sure we render the column of the target table sqmPath,
mapping = (EmbeddableValuedModelPart) ( (ManagedMappingType) modelPartContainer.getPartMappingType() ).findSubPart( modelPartContainer,
sqmPath.getReferencedPathSource().getPathName(), sqlAstCreationState
treatTarget ).findSubPart( sqmPath.getReferencedPathSource().getPathName(), treatTarget );
);
}
else {
mapping = (EmbeddableValuedModelPart) modelPartContainer.findSubPart(
sqmPath.getReferencedPathSource().getPathName(),
treatTarget
);
}
return new EmbeddableValuedPathInterpretation<>( return new EmbeddableValuedPathInterpretation<>(
mapping.toSqlExpression( mapping.toSqlExpression(

View File

@ -162,7 +162,9 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
// we try to make use of it and the FK model part if possible based on the inferred mapping // we try to make use of it and the FK model part if possible based on the inferred mapping
if ( mapping instanceof EntityAssociationMapping ) { if ( mapping instanceof EntityAssociationMapping ) {
final EntityAssociationMapping associationMapping = (EntityAssociationMapping) mapping; final EntityAssociationMapping associationMapping = (EntityAssociationMapping) mapping;
final ModelPart keyTargetMatchPart = associationMapping.getKeyTargetMatchPart(); final ModelPart keyTargetMatchPart = associationMapping.getForeignKeyDescriptor().getPart(
associationMapping.getSideNature()
);
if ( associationMapping.isFkOptimizationAllowed() ) { if ( associationMapping.isFkOptimizationAllowed() ) {
final boolean forceUsingForeignKeyAssociationSidePart; final boolean forceUsingForeignKeyAssociationSidePart;

View File

@ -628,8 +628,8 @@ public class SqmQuerySpec<T> extends SqmQueryPart<T>
} }
public boolean groupByClauseContains(NavigablePath path) { public boolean groupByClauseContains(NavigablePath path) {
for ( SqmExpression<?> expression : groupByClauseExpressions ) { for ( final SqmExpression<?> expression : groupByClauseExpressions ) {
if ( expression instanceof SqmPath && ( (SqmPath<?>) expression ).getNavigablePath() == path ) { if ( expression instanceof SqmPath && ( (SqmPath<?>) expression ).getNavigablePath().isParentOrEqual( path ) ) {
return true; return true;
} }
} }

View File

@ -52,15 +52,15 @@ public class MapIssueTest {
} }
@Test @Test
public void testMapKeyJoinIsIncluded(SessionFactoryScope scope) { public void testMapKeyJoinIsOmitted(SessionFactoryScope scope) {
SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear(); statementInspector.clear();
scope.inTransaction( scope.inTransaction(
s -> { s -> {
s.createQuery( "select c from MapOwner as o join o.contents c join c.relationship r where r.id is not null" ).list(); s.createQuery( "select c from MapOwner as o join o.contents c join c.relationship r where r.id is not null" ).list();
statementInspector.assertExecutedCount( 1 ); statementInspector.assertExecutedCount( 1 );
// Assert 3 joins, collection table, collection element and relationship // Assert 2 joins, collection table and collection element. No need to join the relationship because it is not nullable
statementInspector.assertNumberOfJoins( 0, 3 ); statementInspector.assertNumberOfJoins( 0, 2 );
} }
); );
} }

View File

@ -152,8 +152,9 @@ public class EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest {
.getSingleResult(); .getSingleResult();
statementInspector.assertExecutedCount( 2 ); statementInspector.assertExecutedCount( 2 );
// The join to the target table PARENT for Male#parent is added since it's explicitly joined in HQL // The join to the target table PARENT for Male#parent is avoided,
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 2 ); // because the FK in the collection table is not-null and data from the target table is not needed
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 1 );
statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 3 ); statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 3 );
assertThat( son.getParent(), CoreMatchers.notNullValue() ); assertThat( son.getParent(), CoreMatchers.notNullValue() );