diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java index 69c3423a8625..090cb1db5327 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java @@ -273,6 +273,7 @@ private static boolean hasCustomEventListeners(EventSource source) { // Bean Validation adds a PRE_DELETE listener // and Envers adds a POST_DELETE listener return fss.eventListenerGroup_PRE_DELETE.count() > 0 + || fss.eventListenerGroup_POST_COMMIT_DELETE.count() > 0 || fss.eventListenerGroup_POST_DELETE.count() > 1 || fss.eventListenerGroup_POST_DELETE.count() == 1 && !(fss.eventListenerGroup_POST_DELETE.listeners().iterator().next() 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 66991958223b..dcd9d91959fc 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 @@ -67,4 +67,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 c10fa072d3ba..da89e19458e6 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 @@ -19,6 +19,8 @@ import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Base descriptor, within the mapping model, for any part of the * application's domain model: an attribute, an entity identifier, @@ -146,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 1acf7cbb8531..88266080cc17 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 @@ -62,7 +62,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, @@ -112,10 +112,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 7374c743f118..e812ec8f3bf2 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; @@ -137,6 +138,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 dda8c317bab5..7375dfbe9f8c 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 @@ -873,6 +873,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/SqmJdbcExecutionContextAdapter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java index b2635b98350d..76853d86382d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java @@ -75,4 +75,8 @@ public boolean hasQueryExecutionToBeAddedToStatistics() { return true; } + @Override + public boolean upgradeLocks() { + return true; + } } 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..0c2fcaf92480 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmPathVisitor.java @@ -0,0 +1,91 @@ +/* + * 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) { + super( null ); + 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 88d3e9caac79..b3903b3d6880 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; @@ -47,13 +48,17 @@ 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.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.select.SqmOrderByClause; import org.hibernate.query.sqm.tree.select.SqmQueryPart; -import org.hibernate.query.sqm.tree.jpa.ParameterCollector; +import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +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; @@ -69,6 +74,9 @@ 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; + /** * Helper utilities for dealing with SQM * @@ -119,31 +127,110 @@ public static IllegalQueryOperationException expectingNonSelect(SqmStatement ); } - public static boolean needsTargetTableMapping( + /** + * 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 ModelPartContainer getTargetMappingIfNeeded( SqmPath sqmPath, ModelPartContainer modelPartContainer, SqmToSqlAstConverter sqlAstCreationState) { - final Clause currentClause = sqlAstCreationState.getCurrentClauseStack().getCurrent(); - return ( currentClause == Clause.GROUP || currentClause == Clause.SELECT || currentClause == Clause.ORDER || currentClause == Clause.HAVING ) - && modelPartContainer.getPartMappingType() != modelPartContainer - && sqmPath.getLhs() instanceof SqmFrom - && modelPartContainer.getPartMappingType() instanceof ManagedMappingType - && ( groupByClauseContains( sqlAstCreationState.getCurrentSqmQueryPart(), sqmPath.getNavigablePath() ) - || isNonOptimizableJoin( sqmPath.getLhs() ) ); + 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 boolean groupByClauseContains(SqmQueryPart sqmQueryPart, NavigablePath path) { - return sqmQueryPart.isSimpleQueryPart() && sqmQueryPart.getFirstQuerySpec().groupByClauseContains( path ); + 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; } - private static boolean isNonOptimizableJoin(SqmPath sqmPath) { + /** + * Utility that returns {@code false} when the provided {@link SqmPath sqmPath} is + * a join that cannot be dereferenced through the foreign key on the associated table, + * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} + * or one that has an explicit on clause predicate. + */ + public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { if ( sqmPath instanceof SqmJoin ) { - final SqmJoinType sqmJoinType = ( (SqmJoin) sqmPath ).getSqmJoinType(); - return sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT; + final SqmJoin sqmJoin = (SqmJoin) sqmPath; + switch ( sqmJoin.getSqmJoinType() ) { + case INNER: + case LEFT: + return !( sqmJoin instanceof SqmQualifiedJoin) + || ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null; + default: + return false; + } } 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) { @@ -523,7 +610,7 @@ public static SqmStatement.ParameterResolutions resolveParameters(SqmStatement extends Base private boolean negativeAdjustment; private final Set visitedAssociationKeys = new HashSet<>(); + private final HashMap, Object> metadata = new HashMap<>(); private final MappingMetamodel domainModel; public BaseSqmToSqlAstConverter( @@ -3951,10 +3953,6 @@ public Expression visitQualifiedEntityJoin(SqmEntityJoin sqmJoin) { throw new InterpretationException( "SqmEntityJoin not yet resolved to TableGroup" ); } - private boolean isJoinWithPredicate(SqmFrom path) { - return path instanceof SqmQualifiedJoin && ( (SqmQualifiedJoin) path ).getJoinPredicate() != null; - } - private Expression visitTableGroup(TableGroup tableGroup, SqmFrom path) { final ModelPartContainer tableGroupModelPart = tableGroup.getModelPart(); @@ -3981,9 +3979,9 @@ private Expression visitTableGroup(TableGroup tableGroup, SqmFrom path) { // expansion to all target columns for select and group by clauses is handled in EntityValuedPathInterpretation if ( entityValuedModelPart instanceof EntityAssociationMapping && ( (EntityAssociationMapping) entityValuedModelPart ).isFkOptimizationAllowed() - && !isJoinWithPredicate( path ) ) { + && isFkOptimizationAllowed( path ) ) { // If the table group uses an association mapping that is not a one-to-many, - // we make use of the FK model part - unless the path is a join with an explicit predicate, + // we make use of the FK model part - unless the path is a non-optimizable join, // for which we should always use the target's identifier to preserve semantics final EntityAssociationMapping associationMapping = (EntityAssociationMapping) entityValuedModelPart; final ModelPart targetPart = associationMapping.getForeignKeyDescriptor().getPart( @@ -8212,6 +8210,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 118e66226b99..6f957f18bade 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 6c62d15a2f2e..e99f68e61c85 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 @@ -11,7 +11,11 @@ import java.util.function.Consumer; import org.hibernate.metamodel.MappingMetamodel; -import org.hibernate.metamodel.mapping.*; +import org.hibernate.metamodel.mapping.BasicValuedModelPart; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.SemanticException; @@ -29,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 @@ -76,24 +81,14 @@ public static BasicValuedPathInterpretation from( } } - final BasicValuedModelPart mapping; - if ( needsTargetTableMapping( sqmPath, modelPartContainer, sqlAstCreationState ) ) { - // In the select, group by, order by and having clause we have to make sure we render - // the column of the target table, never the FK column, if the lhs is a join type that - // requires it (right, full) or if this path is contained in group by clause - 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( @@ -108,6 +103,7 @@ public static BasicValuedPathInterpretation from( throw new SemanticException( "`" + 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/EmbeddableValuedExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedExpression.java index d54b8c3deb2f..d477397522da 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EmbeddableValuedExpression.java @@ -15,6 +15,7 @@ import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlAstCreationState; @@ -46,7 +47,7 @@ public EmbeddableValuedExpression( assert mapping != null; assert sqlExpression != null; assert mapping.getEmbeddableTypeDescriptor().getNumberOfAttributeMappings() == sqlExpression.getExpressions().size(); - this.navigablePath = baseNavigablePath.append( mapping.getPartName(), Long.toString( System.nanoTime() ) ); + this.navigablePath = baseNavigablePath.append( mapping.getPartName(), SqmCreationHelper.acquireUniqueAlias()); this.mapping = mapping; this.sqlExpression = sqlExpression; } 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 18f5bbc6d0a0..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,22 +64,12 @@ else if ( lhs.getNodeType() instanceof EntityDomainType ) { } final ModelPartContainer modelPartContainer = tableGroup.getModelPart(); - final EmbeddableValuedModelPart mapping; - if ( needsTargetTableMapping( sqmPath, modelPartContainer, sqlAstCreationState ) ) { - // In the select, group by, order by and having clause we have to make sure we render - // the column of the target table, never the FK column, if the lhs is a join type that - // requires it (right, full) or if this path is contained in group by clause - 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 c562a161a50b..ad5510eb9d70 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 @@ -158,7 +158,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/AbstractSqmDmlStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java index f183c02fc2de..950830443810 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java @@ -15,6 +15,7 @@ import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmQuerySource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -85,21 +86,21 @@ public JpaCteCriteria getCteCriteria(String cteName) { @Override public JpaCteCriteria with(AbstractQuery criteria) { - return withInternal( Long.toString( System.nanoTime() ), criteria ); + return withInternal( SqmCreationHelper.acquireUniqueAlias(), criteria ); } @Override public JpaCteCriteria withRecursiveUnionAll( AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer) { - return withInternal( Long.toString( System.nanoTime() ), baseCriteria, false, recursiveCriteriaProducer ); + return withInternal( SqmCreationHelper.acquireUniqueAlias(), baseCriteria, false, recursiveCriteriaProducer ); } @Override public JpaCteCriteria withRecursiveUnionDistinct( AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer) { - return withInternal( Long.toString( System.nanoTime() ), baseCriteria, true, recursiveCriteriaProducer ); + return withInternal( SqmCreationHelper.acquireUniqueAlias(), baseCriteria, true, recursiveCriteriaProducer ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java index e28709282583..fa082bed4c40 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java @@ -19,6 +19,7 @@ import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.AbstractSqmNode; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; @@ -97,21 +98,21 @@ public JpaCteCriteria getCteCriteria(String cteName) { @Override public JpaCteCriteria with(AbstractQuery criteria) { - return withInternal( Long.toString( System.nanoTime() ), criteria ); + return withInternal( SqmCreationHelper.acquireUniqueAlias(), criteria ); } @Override public JpaCteCriteria withRecursiveUnionAll( AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer) { - return withInternal( Long.toString( System.nanoTime() ), baseCriteria, false, recursiveCriteriaProducer ); + return withInternal( SqmCreationHelper.acquireUniqueAlias(), baseCriteria, false, recursiveCriteriaProducer ); } @Override public JpaCteCriteria withRecursiveUnionDistinct( AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer) { - return withInternal( Long.toString( System.nanoTime() ), baseCriteria, true, recursiveCriteriaProducer ); + return withInternal( SqmCreationHelper.acquireUniqueAlias(), baseCriteria, true, recursiveCriteriaProducer ); } @Override 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 b3d838ed69ff..6ed60031bac1 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,10 +26,11 @@ import org.hibernate.query.criteria.JpaSelection; 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.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; @@ -701,9 +703,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/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java index 34e05bfd775e..1f10a1b76b39 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java @@ -90,4 +90,12 @@ default boolean hasQueryExecutionToBeAddedToStatistics() { return false; } + /** + * Does this query return objects that might be already cached + * by the session, whose lock mode may need upgrading + */ + default boolean upgradeLocks(){ + return false; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java index 4e75bf3b3c98..4b124d194683 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java @@ -571,7 +571,7 @@ && getEntityKey().getIdentifier().equals( executionContext.getEntityId() ) ) { } private void upgradeLockMode(RowProcessingState rowProcessingState) { - if ( lockMode != LockMode.NONE ) { + if ( lockMode != LockMode.NONE && rowProcessingState.upgradeLocks() ) { final EntityEntry entry = rowProcessingState.getSession().getPersistenceContextInternal() .getEntry( entityInstance ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java index 846228ac8c6e..790f6df236ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java @@ -201,4 +201,9 @@ public Initializer resolveInitializer(NavigablePath path) { public boolean hasCollectionInitializers() { return this.initializers.hasCollectionInitializers(); } + + @Override + public boolean upgradeLocks() { + return executionContext.upgradeLocks(); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/limit/Oracle12LimitTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/limit/Oracle12LimitTest.java index 82b1720434b3..f19128709be0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/limit/Oracle12LimitTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/limit/Oracle12LimitTest.java @@ -19,6 +19,7 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; @RequiresDialect(value = OracleDialect.class, majorVersion = 12) @@ -42,11 +43,12 @@ public void testLimit(SessionFactoryScope scope) { final Root personRoot = criteriaquery.from( Person.class ); final Join travels = personRoot.join( "travels", JoinType.LEFT ); - criteriaquery.select( personRoot ). + final Path destination = travels.get( "destination" ); + criteriaquery.multiselect( personRoot, destination ). where( criteriabuilder.or( criteriabuilder.equal( personRoot.get( "name" ), "A" ) ) ) .distinct( true ); - criteriaquery.orderBy( criteriabuilder.desc( criteriabuilder.upper( travels.get( "destination" ) ) ) ); + criteriaquery.orderBy( criteriabuilder.desc( criteriabuilder.upper( destination ) ) ); final TypedQuery createQuery = session.createQuery( criteriaquery ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java new file mode 100644 index 000000000000..31d9b9fb14d0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java @@ -0,0 +1,96 @@ +package org.hibernate.orm.test.locking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; + +import org.hibernate.LockMode; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Version; + +@DomainModel(annotatedClasses = { + OptimisticAndPessimisticLockTest.EntityA.class +}) +@SessionFactory +@TestForIssue(jiraKey = "HHH-16461") +public class OptimisticAndPessimisticLockTest { + + public Stream pessimisticLockModes() { + return Stream.of(LockMode.UPGRADE_NOWAIT, LockMode.PESSIMISTIC_WRITE, LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_FORCE_INCREMENT); + } + + @ParameterizedTest + @MethodSource(value = "pessimisticLockModes") + public void upgradeFromOptimisticToPessimisticLock(LockMode pessimisticLockMode, SessionFactoryScope scope) { + Integer id = scope.fromTransaction( session -> { + EntityA entityA1 = new EntityA(); + entityA1.setPropertyA( 1 ); + session.persist( entityA1 ); + return entityA1.getId(); + } ); + scope.inTransaction( session -> { + EntityA entityA1 = session.find( EntityA.class, id ); + + // Do a concurrent change that will update the @Version property + scope.inTransaction( session2 -> { + var concurrentEntityA1 = session2.find( EntityA.class, id ); + concurrentEntityA1.setPropertyA( concurrentEntityA1.getPropertyA() + 1 ); + } ); + + // Refresh the entity with concurrent changes and upgrade the lock + session.refresh( entityA1, pessimisticLockMode ); + + entityA1.setPropertyA( entityA1.getPropertyA() * 2 ); + } ); + scope.inTransaction( session -> { + EntityA entityA1 = session.find( EntityA.class, id ); + assertThat( entityA1.getPropertyA() ).isEqualTo( ( 1 + 1 ) * 2 ); + } ); + } + + @Entity(name = "EntityA") + public static class EntityA { + + @Id + @GeneratedValue + Integer id; + + @Version + long version; + + int propertyA; + + public EntityA() { + } + + public EntityA(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public long getVersion() { + return version; + } + + public int getPropertyA() { + return propertyA; + } + + public void setPropertyA(int propertyA) { + this.propertyA = propertyA; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java index 90e33523fb72..92b79bc67bdf 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java @@ -80,6 +80,34 @@ public void testIsNotNull(SessionFactoryScope scope) { } ); } + @Test + public void testDereferenceIsNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "left join book.author a " + + "where a.id is null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ); + assertThat( resultList.get( 0 ).getTitle() ).isEqualTo( "Unknown Author" ); + } ); + } + + @Test + public void testDereferenceIsNotNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "left join book.author a " + + "where a.id is not null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + assertThat( resultList.stream().map( b -> b.title ) ).contains( "The Shining", "The Colour Out of Space" ); + } ); + } + @Test public void testIsNotNullWithCondition(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -108,6 +136,34 @@ public void testIsNullWithCondition(SessionFactoryScope scope) { } ); } + @Test + public void testDereferenceIsNotWithCondition(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "left join book.author a with a.alive = true " + + "where a.id is not null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ); + assertThat( resultList.get( 0 ).getTitle() ).isEqualTo( "The Shining" ); + } ); + } + + @Test + public void testDereferenceIsNullWithCondition(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "left join book.author a with a.alive = true " + + "where a.id is null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + assertThat( resultList.stream().map( b -> b.title ) ).contains( "Unknown Author", "The Colour Out of Space" ); + } ); + } + @Entity( name = "Book" ) public static class Book { @Id diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/ManyToManyGroupByOrderByTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ManyToManyGroupByOrderByTest.java new file mode 100644 index 000000000000..674371ddbf62 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/ManyToManyGroupByOrderByTest.java @@ -0,0 +1,210 @@ +/* + * 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.orm.test.query; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Tuple; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + ManyToManyGroupByOrderByTest.Person.class, + ManyToManyGroupByOrderByTest.Cat.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17837" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18202" ) +public class ManyToManyGroupByOrderByTest { + @Test + public void testSelectEntity(SessionFactoryScope scope) { + // explicit join group by + scope.inTransaction( session -> { + final Person result = session.createQuery( + "select owner from Cat cat join cat.owners owner group by owner", + Person.class + ).getSingleResult(); + assertThat( result.getId() ).isEqualTo( 1L ); + } ); + // explicit join group by + order by + scope.inTransaction( session -> { + final Person result = session.createQuery( + "select owner from Cat cat join cat.owners owner group by owner order by owner", + Person.class + ).getSingleResult(); + assertThat( result.getId() ).isEqualTo( 1L ); + } ); + // implicit join group by + scope.inTransaction( session -> { + final Person result = session.createQuery( + "select element(cat.owners) from Cat cat group by element(cat.owners)", + Person.class + ).getSingleResult(); + assertThat( result.getId() ).isEqualTo( 1L ); + } ); + // implicit join group by + order by + scope.inTransaction( session -> { + final Person result = session.createQuery( + "select element(cat.owners) from Cat cat group by element(cat.owners) order by element(cat.owners)", + Person.class + ).getSingleResult(); + assertThat( result.getId() ).isEqualTo( 1L ); + } ); + } + + @Test + public void testSelectAssociationId(SessionFactoryScope scope) { + // explicit join group by + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select owner.id, owner.name from Cat cat join cat.owners owner group by owner", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + assertThat( result.get( 1, String.class ) ).isEqualTo( "Marco" ); + } ); + // explicit join group by + order by + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select owner.id, owner.name from Cat cat join cat.owners owner group by owner order by owner", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + assertThat( result.get( 1, String.class ) ).isEqualTo( "Marco" ); + } ); + // implicit join group by + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select element(cat.owners).id from Cat cat group by element(cat.owners)", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + } ); + // implicit join group by + order by + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select element(cat.owners).id from Cat cat group by element(cat.owners) order by element(cat.owners)", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + } ); + } + + @Test + public void testDistinctAndAggregates(SessionFactoryScope scope) { + // explicit join distinct + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select distinct owner.id from Cat cat join cat.owners owner group by owner.id order by owner.id", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + } ); + // explicit join distinct + aggregate + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select distinct min(owner.id), cat.id from Cat cat join cat.owners owner group by cat.id order by min(owner.id), cat.id", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + } ); + // implicit join distinct + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select distinct element(cat.owners).id from Cat cat group by element(cat.owners).id order by element(cat.owners).id", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + } ); + // implicit join distinct + aggregate + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select distinct min(element(cat.owners).id), cat.id from Cat cat group by cat.id order by min(element(cat.owners).id), cat.id", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Long.class ) ).isEqualTo( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Cat cat = new Cat(); + final Person owner = new Person( 1L, "Marco" ); + cat.addToOwners( owner ); + session.persist( owner ); + session.persist( cat ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from Cat" ).executeUpdate(); + session.createMutationQuery( "delete from Person" ).executeUpdate(); + } ); + } + + @Entity( name = "Person" ) + static class Person { + @Id + private Long id; + + private String name; + + public Person() { + } + + public Person(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToMany( mappedBy = "owners" ) + protected Set ownedCats = new HashSet<>(); + + public Long getId() { + return id; + } + } + + @Entity( name = "Cat" ) + static class Cat { + @Id + @GeneratedValue + protected Long id; + + @JoinTable + @ManyToMany + private Set owners = new HashSet(); + + public Set getOwners() { + return this.owners; + } + + public void addToOwners(Person person) { + owners.add( person ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/RightJoinNullnessPredicateQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/RightJoinNullnessPredicateQueryTest.java new file mode 100644 index 000000000000..de8ca1ec840c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/RightJoinNullnessPredicateQueryTest.java @@ -0,0 +1,172 @@ +/* + * 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.orm.test.query; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + RightJoinNullnessPredicateQueryTest.RelatedEntity.class, + RightJoinNullnessPredicateQueryTest.MainEntity.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17379" ) +public class RightJoinNullnessPredicateQueryTest { + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final RelatedEntity related = new RelatedEntity( 1L ); + session.persist( related ); + session.persist( new RelatedEntity( 2L ) ); + session.persist( new MainEntity( 3L, related ) ); + session.persist( new MainEntity( 4L, null ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from MainEntity" ).executeUpdate(); + session.createMutationQuery( "delete from RelatedEntity" ).executeUpdate(); + } ); + } + + @Test + public void testRightJoinIsNotNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List result = session.createQuery( + "select r.id from MainEntity m right join m.related r where r is not null", + Long.class + ).getResultList(); + assertThat( result ).hasSize( 2 ); + assertThat( result ).contains( 1L, 2L ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testFullJoinIsNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List result = session.createQuery( + "select r.id from MainEntity m full join m.related r where r is null", + Long.class + ).getResultList(); + assertThat( result ).hasSize( 1 ); + assertThat( result ).containsNull(); + } ); + } + + @Test + public void testDereferenceIsNotNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List result = session.createQuery( + "select r.id from MainEntity m right join m.related r where r.id is not null", + Long.class + ).getResultList(); + assertThat( result ).hasSize( 2 ); + assertThat( result ).contains( 1L, 2L ); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsFullJoin.class ) + public void testDereferenceIsNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List result = session.createQuery( + "select r.id from MainEntity m full join m.related r where r.id is null", + Long.class + ).getResultList(); + assertThat( result ).hasSize( 1 ); + assertThat( result ).containsNull(); + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-17397" ) + public void testRightJoinCount(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long result = session.createQuery( + "select count(r) from MainEntity m right join m.related r", + Long.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 2L ); + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-17397" ) + public void testDereferenceCount(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long result = session.createQuery( + "select count(r.id) from MainEntity m right join m.related r", + Long.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 2L ); + } ); + } + + @Entity( name = "RelatedEntity" ) + public static class RelatedEntity { + @Id + private Long id; + + public RelatedEntity() { + } + + public RelatedEntity(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + } + + + @Entity( name = "MainEntity" ) + public static class MainEntity { + @Id + private Long id; + + @OneToOne + private RelatedEntity related; + + public MainEntity() { + } + + public MainEntity(Long id, RelatedEntity related) { + this.id = id; + this.related = related; + } + + public Long getId() { + return id; + } + + public RelatedEntity getRelated() { + return related; + } + } +}