diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/BasicValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/BasicValuedModelPart.java index a98d27a41885..babd435f58e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/BasicValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/BasicValuedModelPart.java @@ -59,4 +59,9 @@ default int forEachSelectable(SelectableConsumer consumer) { default boolean hasPartitionedSelectionMapping() { return isPartitioned(); } + + @Override + default BasicValuedModelPart asBasicValuedModelPart() { + return this; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java index e84b8143c07b..a9ef7203acef 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityAssociationMapping.java @@ -6,6 +6,8 @@ */ package org.hibernate.metamodel.mapping; +import java.util.Set; + import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; /** @@ -21,6 +23,8 @@ default String getFetchableName() { EntityMappingType getAssociatedEntityMappingType(); + Set getTargetKeyPropertyNames(); + /** * The model sub-part relative to the associated entity type that is the target * of this association's foreign-key diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java index 176c4ab8d172..b8e12bbe9cfe 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java @@ -148,6 +148,11 @@ default EntityMappingType asEntityMappingType(){ return null; } + @Nullable + default BasicValuedModelPart asBasicValuedModelPart() { + return null; + } + /** * A short hand form of {@link #breakDownJdbcValues(Object, int, Object, Object, JdbcValueBiConsumer, SharedSessionContractImplementor)}, * that passes 0 as offset and null for the two values {@code X} and {@code Y}. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java index 01e6de36b8b4..27c3fc890c5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java @@ -60,7 +60,7 @@ public abstract class AbstractEntityCollectionPart implements EntityCollectionPa private final EntityMappingType associatedEntityTypeDescriptor; private final NotFoundAction notFoundAction; - private final Set targetKeyPropertyNames; + protected final Set targetKeyPropertyNames; public AbstractEntityCollectionPart( Nature nature, @@ -110,10 +110,6 @@ public EntityMappingType getMappedType() { return getAssociatedEntityMappingType(); } - protected Set getTargetKeyPropertyNames() { - return targetKeyPropertyNames; - } - @Override public NavigableRole getNavigableRole() { return navigableRole; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java index d197318c022f..ff4ecd2c5fd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java @@ -7,6 +7,7 @@ package org.hibernate.metamodel.mapping.internal; import java.util.Locale; +import java.util.Set; import java.util.function.Consumer; import org.hibernate.annotations.NotFoundAction; @@ -135,6 +136,11 @@ public ModelPart findSubPart(String name, EntityMappingType targetType) { return super.findSubPart( name, targetType ); } + @Override + public Set getTargetKeyPropertyNames() { + return targetKeyPropertyNames; + } + @Override public int breakDownJdbcValues( Object domainValue, 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 00aedb2f71b4..4687df0c388b 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 @@ -891,6 +891,7 @@ public String getTargetKeyPropertyName() { return targetKeyPropertyName; } + @Override public Set getTargetKeyPropertyNames() { return targetKeyPropertyNames; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmPathVisitor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmPathVisitor.java new file mode 100644 index 000000000000..c91f48ffd9e8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmPathVisitor.java @@ -0,0 +1,90 @@ +/* + * 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.query.sqm.internal; + +import java.util.function.Consumer; + +import org.hibernate.metamodel.model.domain.DiscriminatorSqmPath; +import org.hibernate.metamodel.model.domain.internal.EntityDiscriminatorSqmPath; +import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker; +import org.hibernate.query.sqm.tree.domain.NonAggregatedCompositeSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; +import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; + +/** + * Generic {@link org.hibernate.query.sqm.SemanticQueryWalker} that applies the provided + * {@link Consumer} to all {@link SqmPath paths} encountered during visitation. + * + * @author Marco Belladelli + */ +public class SqmPathVisitor extends BaseSemanticQueryWalker { + private final Consumer> pathConsumer; + + public SqmPathVisitor(Consumer> pathConsumer) { + this.pathConsumer = pathConsumer; + } + + @Override + public Object visitBasicValuedPath(SqmBasicValuedSimplePath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitEmbeddableValuedPath(SqmEmbeddedValuedSimplePath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitEntityValuedPath(SqmEntityValuedSimplePath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitAnyValuedValuedPath(SqmAnyValuedSimplePath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitQualifiedAttributeJoin(SqmAttributeJoin path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitTreatedPath(SqmTreatedPath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitDiscriminatorPath(EntityDiscriminatorSqmPath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitPluralValuedPath(SqmPluralValuedSimplePath path) { + pathConsumer.accept( path ); + return path; + } + + @Override + public Object visitNonAggregatedCompositeValuedPath(NonAggregatedCompositeSimplePath path) { + pathConsumer.accept( path ); + return path; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index b293e1936203..3808c56f7272 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -24,13 +24,14 @@ import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.Bindable; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.IllegalQueryOperationException; @@ -44,22 +45,28 @@ import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.spi.JdbcParameterBySqmParameterAccess; 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.SqmJoinType; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; +import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.query.sqm.tree.select.SqmQueryPart; +import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSortSpecification; import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -73,6 +80,7 @@ import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.query.sqm.tree.jpa.ParameterCollector.collectParameters; /** @@ -126,13 +134,57 @@ public static IllegalQueryOperationException expectingNonSelect(SqmStatement } /** - * Utility that returns {@code true} if the specified {@link SqmPath sqmPath} should be - * dereferenced using the target table mapping, i.e. when the path's lhs is an explicit join. + * Utility that returns the entity association target's mapping type if the specified {@code sqmPath} should + * 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) { - return modelPartContainer.getPartMappingType() != modelPartContainer - && sqmPath.getLhs() instanceof SqmFrom - && modelPartContainer.getPartMappingType() instanceof ManagedMappingType; + public static ModelPartContainer getTargetMappingIfNeeded( + SqmPath sqmPath, + ModelPartContainer modelPartContainer, + 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(), sqlAstCreationState ) + || queryPart.getFirstQuerySpec().orderByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) ) ) { + 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; } /** @@ -156,6 +208,35 @@ public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { return false; } + public static List getGroupByNavigablePaths(SqmQuerySpec querySpec) { + final List> expressions = querySpec.getGroupByClauseExpressions(); + if ( expressions.isEmpty() ) { + return Collections.emptyList(); + } + + final List navigablePaths = new ArrayList<>( expressions.size() ); + final SqmPathVisitor pathVisitor = new SqmPathVisitor( path -> navigablePaths.add( path.getNavigablePath() ) ); + for ( SqmExpression expression : expressions ) { + expression.accept( pathVisitor ); + } + return navigablePaths; + } + + public static List getOrderByNavigablePaths(SqmQuerySpec querySpec) { + final SqmOrderByClause order = querySpec.getOrderByClause(); + if ( order == null || order.getSortSpecifications().isEmpty() ) { + return Collections.emptyList(); + } + + final List sortSpecifications = order.getSortSpecifications(); + final List navigablePaths = new ArrayList<>( sortSpecifications.size() ); + final SqmPathVisitor pathVisitor = new SqmPathVisitor( path -> navigablePaths.add( path.getNavigablePath() ) ); + for ( SqmSortSpecification sortSpec : sortSpecifications ) { + sortSpec.getSortExpression().accept( pathVisitor ); + } + return navigablePaths; + } + public static Map, Map, List>> generateJdbcParamsXref( DomainParameterXref domainParameterXref, JdbcParameterBySqmParameterAccess jdbcParameterBySqmParameterAccess) { 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 8e9e2536e635..55472bebe572 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 @@ -514,6 +514,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base private boolean negativeAdjustment; private final Set visitedAssociationKeys = new HashSet<>(); + private final HashMap, Object> metadata = new HashMap<>(); private final MappingMetamodel domainModel; public BaseSqmToSqlAstConverter( @@ -8389,6 +8390,42 @@ private void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragmen orderByFragments.add( new AbstractMap.SimpleEntry<>( orderByFragment, tableGroup ) ); } + @Override + public M resolveMetadata(S source, Function producer ) { + //noinspection unchecked + return (M) metadata.computeIfAbsent( new MetadataKey<>( source, producer ), k -> producer.apply( source ) ); + } + + static class MetadataKey { + private final S source; + private final Function producer; + + public MetadataKey(S source, Function producer) { + this.source = source; + this.producer = producer; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + final MetadataKey that = (MetadataKey) o; + return source.equals( that.source ) && producer.equals( that.producer ); + } + + @Override + public int hashCode() { + int result = source.hashCode(); + result = 31 * result + producer.hashCode(); + return result; + } + } + @Override public boolean isResolvingCircularFetch() { return resolvingCircularFetch; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java index 7e8f09985aa2..36d367a119b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.sql; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import org.hibernate.LockMode; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java index 378fef24bee4..b19ad9e79678 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.sql; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import org.hibernate.internal.util.collections.Stack; @@ -23,6 +24,8 @@ import org.hibernate.sql.ast.tree.expression.QueryTransformer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import jakarta.annotation.Nullable; + /** * Specialized SemanticQueryWalker (SQM visitor) for producing SQL AST. * @@ -52,4 +55,10 @@ public interface SqmToSqlAstConverter extends SemanticQueryWalker, SqlAs Predicate visitNestedTopLevelPredicate(SqmPredicate predicate); + /** + * Resolve a generic metadata object from the provided source, using the specified producer. + */ + default M resolveMetadata(S source, Function producer) { + return producer.apply( source ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java index 92c21e8d95e3..7253c3743ce5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/BasicValuedPathInterpretation.java @@ -13,7 +13,6 @@ import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; @@ -34,7 +33,8 @@ import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.update.Assignable; -import static org.hibernate.query.sqm.internal.SqmUtil.needsTargetTableMapping; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; +import static org.hibernate.query.sqm.internal.SqmUtil.getTargetMappingIfNeeded; /** * @author Steve Ebersole @@ -81,22 +81,14 @@ public static BasicValuedPathInterpretation from( } } - final BasicValuedModelPart mapping; - if ( needsTargetTableMapping( sqmPath, modelPartContainer ) ) { - // We have to make sure we render the column of the target table - mapping = (BasicValuedModelPart) ( (ManagedMappingType) modelPartContainer.getPartMappingType() ).findSubPart( - sqmPath.getReferencedPathSource().getPathName(), - treatTarget - ); - } - else { - mapping = (BasicValuedModelPart) modelPartContainer.findSubPart( - sqmPath.getReferencedPathSource().getPathName(), - treatTarget - ); - } + // Use the target type to find the sub part if needed, otherwise just use the container + final ModelPart modelPart = getTargetMappingIfNeeded( + sqmPath, + modelPartContainer, + sqlAstCreationState + ).findSubPart( sqmPath.getReferencedPathSource().getPathName(), treatTarget ); - if ( mapping == null ) { + if ( modelPart == null ) { if ( jpaQueryComplianceEnabled ) { // to get the better error, see if we got nothing because of treat handling final ModelPart subPart = tableGroup.getModelPart().findSubPart( @@ -111,6 +103,7 @@ public static BasicValuedPathInterpretation from( throw new UnknownPathException( "Path '" + sqmPath.getNavigablePath() + "' did not reference a known model part" ); } + final BasicValuedModelPart mapping = castNonNull( modelPart.asBasicValuedModelPart() ); final TableReference tableReference = tableGroup.resolveTableReference( sqmPath.getNavigablePath(), mapping, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedPathInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedPathInterpretation.java index 28fc5875b9f6..af23ff2c41a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedPathInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedPathInterpretation.java @@ -13,7 +13,6 @@ import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; @@ -29,7 +28,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; 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 @@ -65,20 +64,12 @@ else if ( lhs.getNodeType() instanceof EntityDomainType ) { } final ModelPartContainer modelPartContainer = tableGroup.getModelPart(); - final EmbeddableValuedModelPart mapping; - if ( needsTargetTableMapping( sqmPath, modelPartContainer ) ) { - // We have to make sure we render the column of the target table - mapping = (EmbeddableValuedModelPart) ( (ManagedMappingType) modelPartContainer.getPartMappingType() ).findSubPart( - sqmPath.getReferencedPathSource().getPathName(), - treatTarget - ); - } - else { - mapping = (EmbeddableValuedModelPart) modelPartContainer.findSubPart( - sqmPath.getReferencedPathSource().getPathName(), - treatTarget - ); - } + // Use the target type to find the sub part if needed, otherwise just use the container + final EmbeddableValuedModelPart mapping = (EmbeddableValuedModelPart) getTargetMappingIfNeeded( + sqmPath, + modelPartContainer, + sqlAstCreationState + ).findSubPart( sqmPath.getReferencedPathSource().getPathName(), treatTarget ); return new EmbeddableValuedPathInterpretation<>( mapping.toSqlExpression( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java index 24d5185afb3a..b66b0732bce6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java @@ -162,7 +162,9 @@ private static EntityValuedPathInterpretation from( // we try to make use of it and the FK model part if possible based on the inferred mapping if ( mapping instanceof EntityAssociationMapping ) { final EntityAssociationMapping associationMapping = (EntityAssociationMapping) mapping; - final ModelPart keyTargetMatchPart = associationMapping.getKeyTargetMatchPart(); + final ModelPart keyTargetMatchPart = associationMapping.getForeignKeyDescriptor().getPart( + associationMapping.getSideNature() + ); if ( associationMapping.isFkOptimizationAllowed() ) { final boolean forceUsingForeignKeyAssociationSidePart; @@ -265,7 +267,7 @@ public static EntityValuedPathInterpretation from( if ( currentClause == Clause.GROUP || currentClause == Clause.ORDER ) { assert sqlAstCreationState.getCurrentSqmQueryPart().isSimpleQueryPart(); final SqmQuerySpec querySpec = sqlAstCreationState.getCurrentSqmQueryPart().getFirstQuerySpec(); - if ( currentClause == Clause.ORDER && !querySpec.groupByClauseContains( navigablePath ) ) { + if ( currentClause == Clause.ORDER && !querySpec.groupByClauseContains( navigablePath, sqlAstCreationState ) ) { // We must ensure that the order by expression be expanded but only if the group by // contained the same expression, and that was expanded as well expandToAllColumns = false; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java index 6ad3b64c3dae..d0ed480aae32 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Set; +import org.hibernate.Internal; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; @@ -25,11 +26,12 @@ import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmNode; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; -import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -729,9 +731,32 @@ private void appendTreatJoins(SqmFrom sqmFrom, StringBuilder sb) { } } - public boolean groupByClauseContains(NavigablePath path) { - for ( SqmExpression expression : groupByClauseExpressions ) { - if ( expression instanceof SqmPath && ( (SqmPath) expression ).getNavigablePath() == path ) { + @Internal + public boolean groupByClauseContains(NavigablePath navigablePath, SqmToSqlAstConverter sqlAstConverter) { + if ( groupByClauseExpressions.isEmpty() ) { + return false; + } + return navigablePathsContain( sqlAstConverter.resolveMetadata( + this, + SqmUtil::getGroupByNavigablePaths + ), navigablePath ); + } + + @Internal + public boolean orderByClauseContains(NavigablePath navigablePath, SqmToSqlAstConverter sqlAstConverter) { + final SqmOrderByClause orderByClause = getOrderByClause(); + if ( orderByClause == null || orderByClause.getSortSpecifications().isEmpty() ) { + return false; + } + return navigablePathsContain( sqlAstConverter.resolveMetadata( + this, + SqmUtil::getOrderByNavigablePaths + ), navigablePath ); + } + + private boolean navigablePathsContain(List navigablePaths, NavigablePath navigablePath) { + for ( NavigablePath path : navigablePaths ) { + if ( path.isParentOrEqual( navigablePath ) ) { return true; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java index 5f58d9dcb506..c735fa79af3d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java @@ -52,15 +52,15 @@ public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { } @Test - public void testMapKeyJoinIsIncluded(SessionFactoryScope scope) { + public void testMapKeyJoinIsOmitted(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); scope.inTransaction( s -> { 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 ); - // Assert 3 joins, collection table, collection element and relationship - statementInspector.assertNumberOfJoins( 0, 3 ); + // Assert 2 joins, collection table and collection element. No need to join the relationship because it is not nullable + statementInspector.assertNumberOfJoins( 0, 2 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/onetoone/bidirectional/EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/onetoone/bidirectional/EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest.java index 0a780c481392..56f23af3ccba 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/onetoone/bidirectional/EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/onetoone/bidirectional/EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest.java @@ -152,8 +152,9 @@ public void testHqlSelectSon(SessionFactoryScope scope) { .getSingleResult(); statementInspector.assertExecutedCount( 2 ); - // The join to the target table PARENT for Male#parent is added since it's explicitly joined in HQL - statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 2 ); + // The join to the target table PARENT for Male#parent is avoided, + // 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 ); assertThat( son.getParent(), CoreMatchers.notNullValue() );