HHH-18202 Fix group/order by fk rendering handling nested paths

Introduced generalized `MetadataKey`-based resolutions with caching in `BaseSqmToSqlAstConverter`
This commit is contained in:
Marco Belladelli 2024-05-31 09:39:41 +02:00
parent 01199d2c1f
commit c08b1b9bf1
7 changed files with 200 additions and 6 deletions

View File

@ -0,0 +1,89 @@
/*
* 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.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<SqmPath<?>> pathConsumer;
public SqmPathVisitor(Consumer<SqmPath<?>> 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(DiscriminatorSqmPath<?> 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;
}
}

View File

@ -54,6 +54,7 @@ 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.expression.SqmTuple;
@ -62,7 +63,9 @@ 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.SqmSelection;
@ -167,7 +170,8 @@ public class SqmUtil {
if ( association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() )
&& ( clause == Clause.GROUP || clause == Clause.ORDER
|| !isFkOptimizationAllowed( sqmPath.getLhs() )
|| queryPart.getFirstQuerySpec().groupByClauseContains( sqmPath.getNavigablePath() ) ) ) {
|| queryPart.getFirstQuerySpec().groupByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState )
|| queryPart.getFirstQuerySpec().orderByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) ) ) {
return association.getAssociatedEntityMappingType();
}
}
@ -210,6 +214,35 @@ public class SqmUtil {
return false;
}
public static List<NavigablePath> getGroupByNavigablePaths(SqmQuerySpec<?> querySpec) {
final List<SqmExpression<?>> expressions = querySpec.getGroupByClauseExpressions();
if ( expressions.isEmpty() ) {
return Collections.emptyList();
}
final List<NavigablePath> 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<NavigablePath> getOrderByNavigablePaths(SqmQuerySpec<?> querySpec) {
final SqmOrderByClause order = querySpec.getOrderByClause();
if ( order == null || order.getSortSpecifications().isEmpty() ) {
return Collections.emptyList();
}
final List<SqmSortSpecification> sortSpecifications = order.getSortSpecifications();
final List<NavigablePath> 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 <T, A> SqmAttributeJoin<T, A> findCompatibleFetchJoin(
SqmFrom<?, T> sqmFrom,
SqmPathSource<A> pathSource,

View File

@ -524,6 +524,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
private boolean negativeAdjustment;
private final Set<AssociationKey> visitedAssociationKeys = new HashSet<>();
private final HashMap<MetadataKey<?, ?>, Object> metadata = new HashMap<>();
private final MappingMetamodel domainModel;
public BaseSqmToSqlAstConverter(
@ -8614,6 +8615,42 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
orderByFragments.add( new AbstractMap.SimpleEntry<>( orderByFragment, tableGroup ) );
}
@Override
public <S, M> M resolveMetadata(S source, Function<S, M> producer ) {
//noinspection unchecked
return (M) metadata.computeIfAbsent( new MetadataKey<>( source, producer ), k -> producer.apply( source ) );
}
static class MetadataKey<S, M> {
private final S source;
private final Function<S, M> producer;
public MetadataKey(S source, Function<S, M> 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;

View File

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

View File

@ -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.Expression;
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.
*
@ -58,4 +61,10 @@ public interface SqmToSqlAstConverter extends SemanticQueryWalker<Object>, SqlAs
Predicate visitNestedTopLevelPredicate(SqmPredicate predicate);
/**
* Resolve a generic metadata object from the provided source, using the specified producer.
*/
default <S, M> M resolveMetadata(S source, Function<S, M> producer) {
return producer.apply( source );
}
}

View File

@ -267,7 +267,7 @@ public class EntityValuedPathInterpretation<T> extends AbstractSqmPathInterpreta
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;

View File

@ -12,6 +12,7 @@ import java.util.HashSet;
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.criteria.JpaSelection;
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.expression.SqmAliasedNodeRef;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.from.SqmAttributeJoin;
@ -627,9 +629,32 @@ public class SqmQuerySpec<T> extends SqmQueryPart<T>
super.appendHqlString( sb );
}
public boolean groupByClauseContains(NavigablePath path) {
for ( final SqmExpression<?> expression : groupByClauseExpressions ) {
if ( expression instanceof SqmPath && ( (SqmPath<?>) expression ).getNavigablePath().isParentOrEqual( 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<NavigablePath> navigablePaths, NavigablePath navigablePath) {
for ( NavigablePath path : navigablePaths ) {
if ( path.isParentOrEqual( navigablePath ) ) {
return true;
}
}