diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index 96095e245454..6f01afee783a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -118,7 +118,7 @@ protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List rootPathsForLocking) { + return new DerbyLockingClauseStrategy( this, lockKind, rowLockStrategy, lockOptions, rootPathsForLocking ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java index 3aefecb45dae..38c77c1a2b65 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java @@ -58,6 +58,7 @@ import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.service.ServiceRegistry; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -85,6 +86,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.sql.Types; +import java.util.Set; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BLOB; @@ -551,8 +553,9 @@ public boolean supportsCommentOn() { protected LockingClauseStrategy buildLockingClauseStrategy( PessimisticLockKind lockKind, RowLockStrategy rowLockStrategy, - LockOptions lockOptions) { - return new DerbyLockingClauseStrategy( this, lockKind, rowLockStrategy, lockOptions ); + LockOptions lockOptions, + Set rootPathsForLocking) { + return new DerbyLockingClauseStrategy( this, lockKind, rowLockStrategy, lockOptions, rootPathsForLocking ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLockingClauseStrategy.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLockingClauseStrategy.java index 261793a9c936..9a44dd6eabd4 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLockingClauseStrategy.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLockingClauseStrategy.java @@ -7,10 +7,13 @@ import org.hibernate.LockOptions; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.RowLockStrategy; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.internal.PessimisticLockKind; import org.hibernate.sql.ast.internal.StandardLockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAppender; +import java.util.Set; + /** * StandardLockingClauseStrategy subclass, specific for Derby. * @@ -21,8 +24,9 @@ public DerbyLockingClauseStrategy( Dialect dialect, PessimisticLockKind lockKind, RowLockStrategy rowLockStrategy, - LockOptions lockOptions) { - super( dialect, lockKind, rowLockStrategy, lockOptions ); + LockOptions lockOptions, + Set rootPathsForLocking) { + super( dialect, lockKind, rowLockStrategy, lockOptions, rootPathsForLocking ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java index aea08621b817..99e6271a04a1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java @@ -166,15 +166,15 @@ protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List rootPathsForLocking) { if ( getVersion().isBefore( 14 ) ) { return NonLockingClauseStrategy.NON_CLAUSE_STRATEGY; } // we'll reuse the StandardLockingClauseStrategy for the collecting // aspect and just handle the special rendering in the SQL AST translator - return super.buildLockingClauseStrategy( lockKind, rowLockStrategy, lockOptions ); + return super.buildLockingClauseStrategy( lockKind, rowLockStrategy, lockOptions, rootPathsForLocking ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java index 8e1d2dcbcdf7..564a04f7cb71 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java @@ -27,6 +27,7 @@ import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.internal.TransactSQLLockingClauseStrategy; import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.select.QuerySpec; @@ -40,7 +41,6 @@ import java.sql.Types; import java.util.Map; -import static org.hibernate.sql.ast.internal.NonLockingClauseStrategy.NON_CLAUSE_STRATEGY; import static org.hibernate.type.SqlTypes.BLOB; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.CLOB; @@ -200,8 +200,7 @@ public boolean qualifyIndexName() { @Override public LockingClauseStrategy getLockingClauseStrategy(QuerySpec querySpec, LockOptions lockOptions) { - // T-SQL uses table-based lock hints and thus does not support FOR UPDATE clause - return NON_CLAUSE_STRATEGY; + return new TransactSQLLockingClauseStrategy( lockOptions.getScope(), querySpec.getRootPathsForLocking() ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 1fd8ed286548..7601ab4ce695 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -114,6 +114,7 @@ import org.hibernate.query.sqm.sql.SqmTranslatorFactory; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ForUpdateFragment; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -2369,14 +2370,15 @@ public LockingClauseStrategy getLockingClauseStrategy(QuerySpec querySpec, LockO default -> throw new IllegalStateException( "Should never happen due to checks above" ); } - return buildLockingClauseStrategy( lockKind, rowLockStrategy, lockOptions ); + return buildLockingClauseStrategy( lockKind, rowLockStrategy, lockOptions, querySpec.getRootPathsForLocking() ); } protected LockingClauseStrategy buildLockingClauseStrategy( PessimisticLockKind lockKind, RowLockStrategy rowLockStrategy, - LockOptions lockOptions) { - return new StandardLockingClauseStrategy( this, lockKind, rowLockStrategy, lockOptions ); + LockOptions lockOptions, + Set rootPathsForLocking) { + return new StandardLockingClauseStrategy( this, lockKind, rowLockStrategy, lockOptions, rootPathsForLocking ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java index 5aa2f194e7a1..2c018ee7decc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java @@ -73,8 +73,11 @@ public void lock( lockOptions.setScope( lockScope ); lockOptions.setTimeOut( timeout ); - final var rootQuerySpec = new QuerySpec( true ); final var entityPath = new NavigablePath( entityToLock.getRootPathName() ); + + final var rootQuerySpec = new QuerySpec( true ); + rootQuerySpec.applyRootPathForLocking( entityPath ); + final var idMapping = entityToLock.getIdentifierMapping(); // NOTE: there are 2 possible ways to handle the select list for the query... diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2SqlAstTranslator.java index e990995fcf57..b45b692b5df5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2SqlAstTranslator.java @@ -116,7 +116,7 @@ protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List sqmStatement, @@ -109,14 +105,12 @@ public static SelectStatement generateMatchingIdSelectStatement( mutatingTableGroup.getNavigablePath(), mutatingTableGroup, sqmConverter, - (selection, jdbcMapping) -> - domainResults.add( - new BasicResult<>( - selection.getValuesArrayPosition(), - null, - jdbcMapping - ) - ) + (selection, jdbcMapping) -> domainResults.add( + new BasicResult<>( + selection.getValuesArrayPosition(), + null, + jdbcMapping + ) ) ); sqmConverter.getProcessingStateStack().pop(); @@ -133,15 +127,11 @@ public static SelectStatement generateMatchingIdSelectStatement( return new SelectStatement( idSelectionQuery, domainResults ); } - /** - * @asciidoc - * - * Generates a query-spec for selecting all ids matching the restriction defined as part - * of the user's update/delete query. This query-spec is generally used: - * - * * to select all the matching ids via JDBC - see {@link MatchingIdSelectionHelper#selectMatchingIds} - * * as a sub-query restriction to insert rows into an "id table" - */ + /// Generates a query-spec for selecting all ids matching the restriction defined as part + /// of the user's update/delete query. This query-spec is generally used: + /// + /// * to select all the matching ids via JDBC - see {@link MatchingIdSelectionHelper#selectMatchingIds} + /// * as a sub-query restriction to insert rows into an "id table" public static SqmSelectStatement generateMatchingIdSelectStatement( SqmDeleteOrUpdateStatement sqmStatement, EntityMappingType entityDescriptor) { @@ -167,59 +157,6 @@ public static SqmSelectStatement generateMatchingIdSelectStatement( nodeBuilder ); } -// -// /** -// * @asciidoc -// * -// * Generates a query-spec for selecting all ids matching the restriction defined as part -// * of the user's update/delete query. This query-spec is generally used: -// * -// * * to select all the matching ids via JDBC - see {@link MatchingIdSelectionHelper#selectMatchingIds} -// * * as a sub-query restriction to insert rows into an "id table" -// */ -// public static QuerySpec generateMatchingIdSelectQuery( -// EntityMappingType targetEntityDescriptor, -// SqmDeleteOrUpdateStatement sqmStatement, -// DomainParameterXref domainParameterXref, -// Predicate restriction, -// MultiTableSqmMutationConverter sqmConverter, -// SessionFactoryImplementor sessionFactory) { -// final EntityDomainType entityDomainType = sqmStatement.getTarget().getModel(); -// if ( LOG.isTraceEnabled() ) { -// LOG.tracef( -// "Starting generation of entity-id SQM selection - %s", -// entityDomainType.getHibernateEntityName() -// ); -// } -// -// final QuerySpec idSelectionQuery = new QuerySpec( true, 1 ); -// -// final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup(); -// idSelectionQuery.getFromClause().addRoot( mutatingTableGroup ); -// -// targetEntityDescriptor.getIdentifierMapping().forEachSelectable( -// (position, selection) -> { -// final TableReference tableReference = mutatingTableGroup.resolveTableReference( -// mutatingTableGroup.getNavigablePath(), -// selection.getContainingTableExpression() -// ); -// final Expression expression = sqmConverter.getSqlExpressionResolver().resolveSqlExpression( -// tableReference, -// selection -// ); -// idSelectionQuery.getSelectClause().addSqlSelection( -// new SqlSelectionImpl( -// position, -// expression -// ) -// ); -// } -// ); -// -// idSelectionQuery.applyPredicate( restriction ); -// -// return idSelectionQuery; -// } /** * Centralized selection of ids matching the restriction of the DELETE 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 358d6a8ea08b..b7fb06b80a08 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 @@ -1645,6 +1645,7 @@ public SelectStatement visitSelectStatement(SqmSelectStatement statement) { finally { this.currentSqmStatement = oldSqmStatement; this.cteContainer = oldCteContainer; + rootPathsForLockingCollector = null; } } @@ -2205,11 +2206,19 @@ private TableGroup findTableGroupByPath(NavigablePath navigablePath) { return getFromClauseAccess().getTableGroup( navigablePath ); } + private Consumer rootPathsForLockingCollector; + @Override public SelectClause visitSelectClause(SqmSelectClause selectClause) { currentClauseStack.push( Clause.SELECT ); try { final SelectClause sqlSelectClause = currentQuerySpec().getSelectClause(); + if ( sqmQueryPartStack.depth() == 1 && currentClauseStack.depth() == 1 ) { + // these 2 conditions combined *should* indicate we have the + // root query-spec of a top-level select statement + rootPathsForLockingCollector = (path) -> + currentQuerySpec().applyRootPathForLocking( path ); + } if ( selectClause.getSelections().isEmpty() ) { final SqmFrom implicitSelection = determineImplicitSelection( (SqmQuerySpec) getCurrentSqmQueryPart() ); visitSelection( 0, new SqmSelection<>( implicitSelection, implicitSelection.nodeBuilder() ) ); @@ -2224,6 +2233,7 @@ public SelectClause visitSelectClause(SqmSelectClause selectClause) { return sqlSelectClause; } finally { + rootPathsForLockingCollector = null; currentClauseStack.pop(); } } @@ -2243,6 +2253,8 @@ public Void visitSelection(SqmSelection sqmSelection) { } private void visitSelection(int index, SqmSelection sqmSelection) { + collectRootPathsForLocking( sqmSelection ); + inferTargetPath( index ); callResultProducers( resultProducers( sqmSelection ) ); if ( statement instanceof SqmInsertSelectStatement @@ -2251,6 +2263,52 @@ && contributesToTopLevelSelectClause() ) { } } + private void collectRootPathsForLocking(SqmSelection sqmSelection) { + if ( rootPathsForLockingCollector == null ) { + return; + } + + collectRootPathsForLocking( sqmSelection.getSelectableNode() ); + } + + private void collectRootPathsForLocking(SqmSelectableNode selectableNode) { + // roughly speaking we only care about 2 cases here: + // 1) entity path - the entity will be locked + // 2) scalar path - the entity from which the path originates will be locked + // + // note, however, that we need to account for both cases as the argument to a dynamic instantiation + + if ( selectableNode instanceof SqmPath selectedPath ) { + collectRootPathsForLocking( selectedPath ); + } + else if ( selectableNode instanceof SqmDynamicInstantiation dynamicInstantiation ) { + collectRootPathsForLocking( dynamicInstantiation ); + } + } + + private void collectRootPathsForLocking(SqmPath selectedPath) { + assert rootPathsForLockingCollector != null; + + if ( selectedPath == null ) { + // typically this comes from paths rooted in a CTE. + // regardless, without a path we cannot evaluate so just return. + return; + } + + if ( selectedPath.getNodeType() instanceof EntityTypeImpl ) { + rootPathsForLockingCollector.accept( selectedPath.getNavigablePath() ); + } + else { + collectRootPathsForLocking( selectedPath.getLhs() ); + } + } + + private void collectRootPathsForLocking(SqmDynamicInstantiation dynamicInstantiation) { + dynamicInstantiation.getArguments().forEach( ( argument ) -> { + collectRootPathsForLocking( argument.getSelectableNode() ); + } ); + } + private void inferTargetPath(int index) { // Only infer the type on the "top level" select clauses // todo: add WriteExpression handling diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/AbstractLockingClauseStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/AbstractLockingClauseStrategy.java new file mode 100644 index 000000000000..efe0f22b647e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/AbstractLockingClauseStrategy.java @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.internal; + +import org.hibernate.Locking; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.LockingClauseStrategy; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/// Base support for LockingClauseStrategy implementations +/// +/// @author Steve Ebersole +public abstract class AbstractLockingClauseStrategy implements LockingClauseStrategy { + protected final Locking.Scope lockingScope; + protected final Set rootsForLocking; + + private Set pathsToLock; + + public AbstractLockingClauseStrategy( + Locking.Scope lockingScope, + Set rootsForLocking) { + this.lockingScope = lockingScope; + this.rootsForLocking = rootsForLocking == null ? Set.of() : rootsForLocking; + } + + @Override + public boolean registerRoot(TableGroup root) { + if ( shouldLockRoot( root ) ) { + trackRoot( root ); + return true; + } + else { + return false; + } + } + + protected boolean shouldLockRoot(TableGroup root) { + // NOTE : the NavigablePath can be null in some cases. + // we don't care about these cases, so easier to just + // handle the nullness here + return root.getNavigablePath() != null && rootsForLocking.contains( root.getNavigablePath() ); + } + + protected void trackRoot(TableGroup root) { + if ( pathsToLock == null ) { + pathsToLock = new HashSet<>(); + } + pathsToLock.add( root.getNavigablePath() ); + } + + @Override + public boolean registerJoin(TableGroupJoin join) { + if ( shouldLockJoin( join.getJoinedGroup() ) ) { + trackJoin( join ); + return true; + } + else { + return false; + } + } + + protected boolean shouldLockJoin(TableGroup joinedGroup) { + // we only want to consider applying locks to joins in 2 cases: + // 1) It is a root path for locking (aka occurs in the domain select-clause) + // 2) It's left-hand side is to be locked + if ( isRootForLocking( joinedGroup ) ) { + return true; + } + else if ( isLhsLocked( joinedGroup ) ) { + if ( lockingScope == Locking.Scope.INCLUDE_COLLECTIONS ) { + // if the TableGroup is an owned (aka, non-inverse) collection, + // and we are to lock collections, track it + if ( joinedGroup.getModelPart() instanceof PluralAttributeMapping attrMapping ) { + if ( !attrMapping.getCollectionDescriptor().isInverse() ) { + // owned collection element-collection + return attrMapping.getElementDescriptor() instanceof BasicValuedCollectionPart; + } + } + } + else if ( lockingScope == Locking.Scope.INCLUDE_FETCHES ) { + return joinedGroup.isFetched(); + } + } + + return false; + } + + protected boolean isRootForLocking(TableGroup joinedGroup) { + return rootsForLocking.contains( joinedGroup.getNavigablePath() ); + } + + protected boolean isLhsLocked(TableGroup joinedGroup) { + // todo (pessimistic-locking) : The use of NavigablePath#parent for LHS here is not ideal. + // However, the alternative is to change the method signature to pass the + // join's LHS which would have a broad impact on Dialects and translators. + // This will possibly miss some cases, but let's start here fow now. + return pathsToLock != null + && pathsToLock.contains( joinedGroup.getNavigablePath().getParent() ); + } + + protected void trackJoin(TableGroupJoin join) { + if ( pathsToLock == null ) { + pathsToLock = new HashSet<>(); + } + pathsToLock.add( join.getNavigablePath() ); + } + + @Override + public Collection getPathsToLock() { + return pathsToLock; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/NonLockingClauseStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/NonLockingClauseStrategy.java index da5ae8472264..49c10c9f573c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/NonLockingClauseStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/NonLockingClauseStrategy.java @@ -4,6 +4,7 @@ */ package org.hibernate.sql.ast.internal; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -23,13 +24,15 @@ public class NonLockingClauseStrategy implements LockingClauseStrategy { public static final NonLockingClauseStrategy NON_CLAUSE_STRATEGY = new NonLockingClauseStrategy(); @Override - public void registerRoot(TableGroup root) { + public boolean registerRoot(TableGroup root) { // nothing to do + return false; } @Override - public void registerJoin(TableGroupJoin join) { + public boolean registerJoin(TableGroupJoin join) { // nothing to do + return false; } @Override @@ -43,12 +46,7 @@ public void render(SqlAppender sqlAppender) { } @Override - public Collection getRootsToLock() { - return List.of(); - } - - @Override - public Collection getJoinsToLock() { + public Collection getPathsToLock() { return List.of(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java index e73c6b35db02..4f4425756190 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java @@ -7,7 +7,6 @@ import jakarta.persistence.Timeout; import org.hibernate.AssertionFailure; import org.hibernate.LockOptions; -import org.hibernate.Locking; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.RowLockStrategy; import org.hibernate.internal.util.collections.CollectionHelper; @@ -17,11 +16,10 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableMappings; import org.hibernate.metamodel.mapping.TableDetails; -import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -31,7 +29,6 @@ import org.hibernate.sql.model.TableMapping; import java.util.ArrayList; -import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -43,21 +40,12 @@ * * @author Steve Ebersole */ -public class StandardLockingClauseStrategy implements LockingClauseStrategy { +public class StandardLockingClauseStrategy extends AbstractLockingClauseStrategy { private final Dialect dialect; private final RowLockStrategy rowLockStrategy; private final PessimisticLockKind lockKind; - private final Locking.Scope lockingScope; private final Timeout timeout; - /** - * @implNote Tracked separately from {@linkplain #rootsToLock} and - * {@linkplain #joinsToLock} to help answer {@linkplain #containsOuterJoins()} - * for {@linkplain RowLockStrategy#NONE cases} where we otherwise don't need to - * track the tables, allowing to avoid the overhead of the Sets. There is a - * slight trade-off in that we need to inspect the from-elements to make that - * determination when we might otherwise not need to - memory versus cpu. - */ private boolean queryHasOuterJoins = false; private Set rootsToLock; @@ -67,21 +55,20 @@ public StandardLockingClauseStrategy( Dialect dialect, PessimisticLockKind lockKind, RowLockStrategy rowLockStrategy, - LockOptions lockOptions) { - // NOTE: previous versions would limit collection based on RowLockStrategy. - // however, this causes problems with the new follow-on locking approach + LockOptions lockOptions, + Set rootsForLocking) { + super( lockOptions.getScope(), rootsForLocking ); assert lockKind != PessimisticLockKind.NONE; this.dialect = dialect; this.rowLockStrategy = rowLockStrategy; this.lockKind = lockKind; - this.lockingScope = lockOptions.getScope(); this.timeout = lockOptions.getTimeout(); } @Override - public void registerRoot(TableGroup root) { + public boolean registerRoot(TableGroup root) { if ( !queryHasOuterJoins ) { if ( CollectionHelper.isNotEmpty( root.getTableReferenceJoins() ) ) { // joined inheritance and/or secondary tables - inherently has outer joins @@ -89,6 +76,12 @@ public void registerRoot(TableGroup root) { } } + return super.registerRoot( root ); + } + + @Override + protected void trackRoot(TableGroup root) { + super.trackRoot( root ); if ( rootsToLock == null ) { rootsToLock = new HashSet<>(); } @@ -96,27 +89,18 @@ public void registerRoot(TableGroup root) { } @Override - public void registerJoin(TableGroupJoin join) { + public boolean registerJoin(TableGroupJoin join) { checkForOuterJoins( join ); + return super.registerJoin( join ); + } - if ( lockingScope == Locking.Scope.INCLUDE_COLLECTIONS ) { - // if the TableGroup is an owned (aka, non-inverse) collection, - // and we are to lock collections, track it - if ( join.getJoinedGroup().getModelPart() instanceof PluralAttributeMapping attrMapping ) { - if ( !attrMapping.getCollectionDescriptor().isInverse() ) { - // owned collection - if ( attrMapping.getElementDescriptor() instanceof BasicValuedCollectionPart ) { - // an element-collection - trackJoin( join ); - } - } - } - } - else if ( lockingScope == Locking.Scope.INCLUDE_FETCHES ) { - if ( join.getJoinedGroup().isFetched() ) { - trackJoin( join ); - } + @Override + protected void trackJoin(TableGroupJoin join) { + super.trackJoin( join ); + if ( joinsToLock == null ) { + joinsToLock = new LinkedHashSet<>(); } + joinsToLock.add( join ); } private void checkForOuterJoins(TableGroupJoin join) { @@ -144,13 +128,6 @@ private boolean hasOuterJoin(TableGroupJoin join) { return false; } - private void trackJoin(TableGroupJoin join) { - if ( joinsToLock == null ) { - joinsToLock = new LinkedHashSet<>(); - } - joinsToLock.add( join ); - } - @Override public boolean containsOuterJoins() { return queryHasOuterJoins; @@ -162,16 +139,6 @@ public void render(SqlAppender sqlAppender) { renderResultSetOptions( sqlAppender ); } - @Override - public Collection getRootsToLock() { - return rootsToLock; - } - - @Override - public Collection getJoinsToLock() { - return joinsToLock; - } - protected void renderLockFragment(SqlAppender sqlAppender) { final String fragment; if ( rowLockStrategy == RowLockStrategy.NONE ) { @@ -179,6 +146,21 @@ protected void renderLockFragment(SqlAppender sqlAppender) { ? dialect.getReadLockString( timeout ) : dialect.getWriteLockString( timeout ); } + else if ( CollectionHelper.isEmpty( rootsToLock ) + && CollectionHelper.isEmpty( joinsToLock ) ) { + // this might happen with locking and scalar queries. e.g. + // session.createQuery( "select p.unitCost * .049 from Product p" ) + // .setLockMode(...) + // + // the spec says: + // > (if) the query returns scalar data ..., the underlying database rows will be locked + // + // so we use a simple `for update`, with no `of`. aka, we treat it the same as RowLockStrategy.NONE + assert CollectionHelper.isEmpty( rootsForLocking ); + fragment = lockKind == PessimisticLockKind.SHARE + ? dialect.getReadLockString( timeout ) + : dialect.getWriteLockString( timeout ); + } else { final String lockItemsFragment = collectLockItems(); fragment = lockKind == PessimisticLockKind.SHARE @@ -194,8 +176,10 @@ private String collectLockItems() { } final List lockItems = new ArrayList<>(); - for ( TableGroup root : rootsToLock ) { - collectLockItems( root, lockItems ); + if ( rootsToLock != null ) { + for ( TableGroup root : rootsToLock ) { + collectLockItems( root, lockItems ); + } } if ( joinsToLock != null ) { for ( TableGroupJoin join : joinsToLock ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TransactSQLLockingClauseStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TransactSQLLockingClauseStrategy.java new file mode 100644 index 000000000000..ef9e626f03f4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TransactSQLLockingClauseStrategy.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.internal; + +import org.hibernate.Locking; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAppender; + +import java.util.Set; + +/// LockingClauseStrategy implementation for T-SQL (SQL Server and Sybase) +/// +/// @author Steve Ebersole +public class TransactSQLLockingClauseStrategy extends AbstractLockingClauseStrategy { + + public TransactSQLLockingClauseStrategy(Locking.Scope lockingScope, Set rootsForLocking) { + super( lockingScope, rootsForLocking ); + } + + @Override + public boolean containsOuterJoins() { + // not used for T-SQL dialects + return false; + } + + @Override + public void render(SqlAppender sqlAppender) { + // not used for T-SQL dialects + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index e382818f8669..2fe0d6e35eef 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -34,9 +34,7 @@ import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.ModelPartContainer; -import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SqlTypedMapping; -import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; import org.hibernate.metamodel.model.domain.ReturnableType; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.internal.SqlFragmentPredicate; @@ -5867,15 +5865,9 @@ else if ( root.isInitialized() ) { } protected void renderRootTableGroup(TableGroup tableGroup, List tableGroupJoinCollector) { - final LockMode effectiveLockMode = getEffectiveLockMode( tableGroup.getSourceAlias() ); + final LockMode effectiveLockMode = determineRootTableGroupLockMode( tableGroup ); renderPrimaryTableReference( tableGroup, effectiveLockMode ); - if ( lockingClauseStrategy != null ) { - if ( getCurrentQueryPart() == lockingTarget ) { - lockingClauseStrategy.registerRoot( tableGroup ); - } - } - if ( tableGroup.isLateral() && !dialect.supportsLateral() ) { addAdditionalWherePredicate( determineLateralEmulationPredicate( tableGroup ) ); } @@ -5899,11 +5891,12 @@ protected void renderRootTableGroup(TableGroup tableGroup, List /** * Called to render the joined TableGroup from a {@linkplain TableGroupJoin} - * @param tableGroup The joined TableGroup + * @param tableGroupJoin The joined TableGroup * @param tableGroupJoinCollector Collector for any nested TableGroupJoins */ - protected void renderJoinedTableGroup(TableGroup tableGroup, Predicate predicate, List tableGroupJoinCollector) { - final LockMode lockModeToApply = determineJoinedTableGroupLockMode( tableGroup ); + protected void renderJoinedTableGroup(TableGroupJoin tableGroupJoin, Predicate predicate, List tableGroupJoinCollector) { + final LockMode lockModeToApply = determineJoinedTableGroupLockMode( tableGroupJoin ); + var tableGroup = tableGroupJoin.getJoinedGroup(); final boolean realTableGroup; int swappedJoinIndex = -1; @@ -5964,7 +5957,7 @@ else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_ } renderPrimaryTableReference( tableGroup, lockModeToApply ); - final List tableGroupJoins; + final List tableGroupJoinJoins; if ( realTableGroup ) { // For real table groups, we collect all normal table group joins within that table group @@ -5972,17 +5965,17 @@ else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_ // This is necessary for at least Derby but is also a lot easier to read renderTableReferenceJoins( tableGroup, lockModeToApply ); if ( tableGroupJoinCollector == null ) { - tableGroupJoins = new ArrayList<>(); - processNestedTableGroupJoins( tableGroup, tableGroupJoins ); + tableGroupJoinJoins = new ArrayList<>(); + processNestedTableGroupJoins( tableGroup, tableGroupJoinJoins ); } else { - tableGroupJoins = null; + tableGroupJoinJoins = null; processNestedTableGroupJoins( tableGroup, tableGroupJoinCollector ); } appendSql( CLOSE_PARENTHESIS ); } else { - tableGroupJoins = null; + tableGroupJoinJoins = null; } // Predicate was already rendered when swappedJoinIndex is not equal to -1 @@ -6011,9 +6004,9 @@ else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_ tableGroupJoinCollector.addAll( tableGroup.getTableGroupJoins() ); } else { - if ( tableGroupJoins != null ) { - for ( TableGroupJoin tableGroupJoin : tableGroupJoins ) { - processTableGroupJoin( tableGroupJoin, null ); + if ( tableGroupJoinJoins != null ) { + for ( TableGroupJoin tableGroupJoinJoin : tableGroupJoinJoins ) { + processTableGroupJoin( tableGroupJoinJoin, null ); } } processTableGroupJoins( tableGroup ); @@ -6028,32 +6021,6 @@ else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_ } } - private LockMode determineJoinedTableGroupLockMode(TableGroup joinedTableGroup) { - final Locking.Scope lockingScope = lockOptions == null ? Locking.Scope.ROOT_ONLY : lockOptions.getScope(); - - if ( lockingScope == Locking.Scope.ROOT_ONLY ) { - return LockMode.NONE; - } - - if ( lockingScope == Locking.Scope.INCLUDE_FETCHES ) { - return joinedTableGroup.isFetched() ? getEffectiveLockMode() : LockMode.NONE; - } - - if ( lockingScope == Locking.Scope.INCLUDE_COLLECTIONS ) { - // if the TableGroup is an owned (aka, non-inverse) collection, lock it - if ( joinedTableGroup.getModelPart() instanceof PluralAttributeMapping attrMapping ) { - if ( !attrMapping.getCollectionDescriptor().isInverse() ) { - // owned collection - if ( attrMapping.getElementDescriptor() instanceof BasicValuedCollectionPart ) { - return getEffectiveLockMode(); - } - } - } - } - - return LockMode.NONE; - } - protected boolean needsLocking(QuerySpec querySpec) { final LockOptions lockOptions = getLockOptions(); return lockOptions != null && lockOptions.getLockMode().isPessimistic(); @@ -6096,6 +6063,29 @@ else if ( tableReference instanceof DerivedTableReference derivedTableReference return false; } + protected LockMode determineRootTableGroupLockMode(TableGroup tableGroup) { + if ( lockingClauseStrategy != null ) { + if ( getCurrentQueryPart() == lockingTarget ) { + if ( lockingClauseStrategy.registerRoot( tableGroup ) ) { + return getEffectiveLockMode( tableGroup.getSourceAlias() ); + } + } + } + return LockMode.NONE; + } + + private LockMode determineJoinedTableGroupLockMode(TableGroupJoin join) { + if ( lockingClauseStrategy != null ) { + if ( getCurrentQueryPart() == lockingTarget ) { + if ( lockingClauseStrategy.registerJoin( join ) ) { + return getEffectiveLockMode( join.getJoinedGroup().getSourceAlias() ); + } + } + } + + return LockMode.NONE; + } + protected void renderDerivedTableReference(DerivedTableReference tableReference) { if ( tableReference.isLateral() ) { if ( dialect.supportsLateral() ) { @@ -6187,7 +6177,8 @@ protected boolean isCorrelated(CteStatement cteStatement) { && !selectStatement.getQueryPart().isRoot(); } - protected boolean renderNamedTableReference(NamedTableReference tableReference, LockMode lockMode) { + protected boolean renderNamedTableReference( + NamedTableReference tableReference, LockMode lockMode) { appendSql( tableReference.getTableExpression() ); registerAffectedTable( tableReference ); renderTableReferenceIdentificationVariable( tableReference ); @@ -6432,15 +6423,10 @@ protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List - * Some dialects do not use a {@code FOR UPDATE (OF)} to apply - * locks - e.g., they apply locks in the {@code FROM} clause. Such - * dialects would return a no-op version of this contract. - *

- * Some dialects support an additional {@code FOR SHARE (OF)} clause - * as well to acquire non-exclusive locks. That is also handled here, - * varied by the requested {@linkplain org.hibernate.LockMode LockMode}. - *

- * Operates in 2 "phases"-

    - *
  1. - * collect tables which are to be locked (based on {@linkplain org.hibernate.Locking.Scope}, - * and other things) - *
  2. - *
  3. - * render the appropriate locking fragment - *
  4. - *
- * - * @see org.hibernate.dialect.Dialect#getLockingClauseStrategy - * - * @author Steve Ebersole - */ +/// Strategy for dealing with locking via a SQL `FOR UPDATE (OF)` +/// clause. +/// +/// Some dialects do not use a `FOR UPDATE (OF)` to apply +/// locks - e.g., they apply locks in the `FROM` clause. Such +/// dialects would return a no-op version of this contract. +/// +/// Some dialects support an additional `FOR SHARE (OF)` clause +/// as well to acquire non-exclusive locks. That is also handled here, +/// varied by the requested {@linkplain org.hibernate.LockMode LockMode}. +/// +/// Operates in 2 "phases"- +/// * collect tables which are to be locked (based on {@linkplain org.hibernate.Locking.Scope}, and other things) +/// * render the appropriate locking fragment +/// +/// @implSpec Note that this is also used to determine and track which +/// tables to lock even for cases (T-SQL e.g.) where a "locking clause" +/// per-se won't be used. In such cases, only the first phase (along +/// with [#shouldLockRoot] and [#shouldLockJoin]) have any impact. +/// +/// @see org.hibernate.dialect.Dialect#getLockingClauseStrategy +/// @see org.hibernate.sql.exec.spi.JdbcSelectWithActionsBuilder +/// +/// @author Steve Ebersole +@Incubating public interface LockingClauseStrategy { - void registerRoot(TableGroup root); - void registerJoin(TableGroupJoin join); + /// Register the given [root][TableGroup] + /// @return Whether the [root][TableGroup] ought to be locked + boolean registerRoot(TableGroup root); + + /// Register the given [join][TableGroupJoin] + /// @return Whether the [join][TableGroupJoin] ought to be locked + boolean registerJoin(TableGroupJoin join); + /// Are any outer joins encountered during registration + /// of [roots][#registerRoot] and [joins][#registerJoin] boolean containsOuterJoins(); + /// For cases where a locking clause is to be used, + /// render that locking clause. void render(SqlAppender sqlAppender); - Collection getRootsToLock(); - Collection getJoinsToLock(); + // All [NavigablePath]s to be locked. + Collection getPathsToLock(); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java index c3d79bf186b7..7dabae246724 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java @@ -5,10 +5,13 @@ package org.hibernate.sql.ast.tree.select; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlAstTreeHelper; import org.hibernate.sql.ast.tree.SqlAstNode; @@ -30,6 +33,8 @@ public class QuerySpec extends QueryPart implements SqlAstNode, PredicateContain private List groupByClauseExpressions = Collections.emptyList(); private Predicate havingClauseRestrictions; + private Set rootPathsForLocking; + public QuerySpec(boolean isRoot) { super( isRoot ); this.fromClause = new FromClause(); @@ -87,6 +92,21 @@ public SelectClause getSelectClause() { return selectClause; } + /// Set of [NavigablePath] references to be considered roots + /// for locking purposes. + public Set getRootPathsForLocking() { + return rootPathsForLocking; + } + + /// Applies a [NavigablePath] to be considered a root for the + /// purpose of potential locking. + public void applyRootPathForLocking(NavigablePath path) { + if ( rootPathsForLocking == null ) { + rootPathsForLocking = new HashSet<>(); + } + rootPathsForLocking.add( path ); + } + public Predicate getWhereClauseRestrictions() { return whereClauseRestrictions; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java index a15043918750..29f8438a8f9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java @@ -4,9 +4,6 @@ */ package org.hibernate.sql.ast.tree.select; -import java.util.Collections; -import java.util.List; - import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.sql.internal.DomainResultProducer; @@ -22,6 +19,9 @@ import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.type.spi.TypeConfiguration; +import java.util.Collections; +import java.util.List; + /** * @author Steve Ebersole */ @@ -33,7 +33,9 @@ public SelectStatement(QueryPart queryPart) { this( queryPart, Collections.emptyList() ); } - public SelectStatement(QueryPart queryPart, List> domainResults) { + public SelectStatement( + QueryPart queryPart, + List> domainResults) { this( null, queryPart, domainResults ); } @@ -122,13 +124,11 @@ public void applySqlSelections(DomainResultCreationState creationState) { public JdbcMappingContainer getExpressionType() { final SelectClause selectClause = queryPart.getFirstQuerySpec().getSelectClause(); final List sqlSelections = selectClause.getSqlSelections(); - switch ( sqlSelections.size() ) { - case 1: - return sqlSelections.get( 0 ).getExpressionType(); - default: - // todo (6.0): At some point we should create an ArrayTupleType and return that - case 0: - return null; + if ( sqlSelections.size() == 1 ) { + return sqlSelections.get( 0 ).getExpressionType(); + } + else { + return null; } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectWithActions.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectWithActions.java index a2d9da1d3e70..787a23e62a8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectWithActions.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectWithActions.java @@ -171,7 +171,7 @@ public static class Builder implements JdbcSelectWithActionsBuilder { protected LockOptions lockOptions; protected QuerySpec lockingTarget; protected LockingClauseStrategy lockingClauseStrategy; - boolean isFollonOnLockStrategy; + boolean isFollowOnLockStrategy; @Override public Builder setPrimaryAction(JdbcSelect primaryAction){ @@ -218,8 +218,8 @@ public Builder setLockingClauseStrategy(LockingClauseStrategy lockingClauseStrat } @Override - public Builder setIsFollowOnLockStrategy(boolean isFollonOnLockStrategy){ - this.isFollonOnLockStrategy = isFollonOnLockStrategy; + public Builder setIsFollowOnLockStrategy(boolean isFollowOnLockStrategy){ + this.isFollowOnLockStrategy = isFollowOnLockStrategy; return this; } @@ -233,7 +233,7 @@ public JdbcSelect build() { ) ); } - if ( isFollonOnLockStrategy ) { + if ( isFollowOnLockStrategy ) { FollowOnLockingAction.apply( lockOptions, lockingTarget, lockingClauseStrategy, this ); } else if ( lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS ) { @@ -341,18 +341,17 @@ public Builder addSecondaryActionPair(PreAction preAction, PostAction postAction // Used by Hibernate Reactive static PreAction[] toPreActionArray(List actions) { - if ( CollectionHelper.isEmpty( actions ) ) { - return null; + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + return actions.toArray( new PreAction[0] ); } - return actions.toArray( new PreAction[0] ); - } - // Used by Hibernate Reactive - static PostAction[] toPostActionArray(List actions) { - if ( CollectionHelper.isEmpty( actions ) ) { - return null; + // Used by Hibernate Reactive + static PostAction[] toPostActionArray(List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + return actions.toArray( new PostAction[0] ); } - return actions.toArray( new PostAction[0] ); - } - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionLockingAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionLockingAction.java index 8322a0533f1c..c3e9a73f69ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionLockingAction.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionLockingAction.java @@ -15,8 +15,6 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.tree.from.FromClause; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcSelectWithActionsBuilder; @@ -29,6 +27,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static org.hibernate.sql.exec.SqlExecLogger.SQL_EXEC_LOGGER; @@ -63,7 +62,7 @@ public static void apply( JdbcSelectWithActionsBuilder jdbcSelectBuilder) { assert lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS; - final var loadedValuesCollector = resolveLoadedValuesCollector( lockingTarget.getFromClause() ); + final var loadedValuesCollector = resolveLoadedValuesCollector( lockingTarget ); // NOTE: we need to set this separately so that it can get incorporated into // the JdbcValuesSourceProcessingState for proper callbacks @@ -140,25 +139,14 @@ protected void performPostAction(ExecutionContext executionContext) { } // Used by Hibernate Reactive - protected static LoadedValuesCollectorImpl resolveLoadedValuesCollector(FromClause fromClause) { - final var fromClauseRoots = fromClause.getRoots(); - if ( fromClauseRoots.size() == 1 ) { - return new LoadedValuesCollectorImpl( - List.of( fromClauseRoots.get( 0 ).getNavigablePath() ) - ); - } - else { - return new LoadedValuesCollectorImpl( - fromClauseRoots.stream().map( TableGroup::getNavigablePath ).toList() - ); - } + protected static LoadedValuesCollectorImpl resolveLoadedValuesCollector(QuerySpec lockingTarget) { + return new LoadedValuesCollectorImpl( lockingTarget.getRootPathsForLocking() ); } // Used by Hibernate Reactive protected static Map> segmentLoadedValues(LoadedValuesCollector loadedValuesCollector) { final Map> map = new IdentityHashMap<>(); - LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map ); - LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map ); + LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedEntities(), map ); if ( map.isEmpty() ) { // NOTE: this may happen with Session#lock routed through SqlAstBasedLockingStrategy. // however, we cannot tell that is the code path from here. @@ -168,29 +156,22 @@ protected static Map> segmentLoadedValues(Loa // Used by Hibernate Reactive protected static class LoadedValuesCollectorImpl implements LoadedValuesCollector { - private final List rootPaths; + private final Set pathsToLock; - private List rootEntitiesToLock; - private List nonRootEntitiesToLock; + private List entitiesToLock; private List collectionsToLock; - private LoadedValuesCollectorImpl(List rootPaths) { - this.rootPaths = rootPaths; + private LoadedValuesCollectorImpl(Set pathsToLock) { + this.pathsToLock = pathsToLock; } @Override public void registerEntity(NavigablePath navigablePath, EntityMappingType entityDescriptor, EntityKey entityKey) { - if ( rootPaths.contains( navigablePath ) ) { - if ( rootEntitiesToLock == null ) { - rootEntitiesToLock = new ArrayList<>(); - } - rootEntitiesToLock.add( new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); - } - else { - if ( nonRootEntitiesToLock == null ) { - nonRootEntitiesToLock = new ArrayList<>(); + if ( pathsToLock.contains( navigablePath ) ) { + if ( entitiesToLock == null ) { + entitiesToLock = new ArrayList<>(); } - nonRootEntitiesToLock.add( new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); + entitiesToLock.add( new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); } } @@ -204,11 +185,8 @@ public void registerCollection(NavigablePath navigablePath, PluralAttributeMappi @Override public void clear() { - if ( rootEntitiesToLock != null ) { - rootEntitiesToLock.clear(); - } - if ( nonRootEntitiesToLock != null ) { - nonRootEntitiesToLock.clear(); + if ( entitiesToLock != null ) { + entitiesToLock.clear(); } if ( collectionsToLock != null ) { collectionsToLock.clear(); @@ -216,13 +194,8 @@ public void clear() { } @Override - public List getCollectedRootEntities() { - return rootEntitiesToLock; - } - - @Override - public List getCollectedNonRootEntities() { - return nonRootEntitiesToLock; + public List getCollectedEntities() { + return entitiesToLock; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java index d06338eaf17f..8ea0f34dd624 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java @@ -25,13 +25,12 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.tree.from.FromClause; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcSelectWithActionsBuilder; -import org.hibernate.sql.exec.spi.StatementAccess; import org.hibernate.sql.exec.spi.LoadedValuesCollector; import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.StatementAccess; import java.sql.Connection; import java.util.ArrayList; @@ -112,10 +111,7 @@ public void performPostAction( try { // collect registrations by entity type final var entitySegments = segmentLoadedValues(); - final Map>> collectionSegments = - lockScope == Locking.Scope.INCLUDE_FETCHES - ? segmentLoadedCollections() - : emptyMap(); + final var collectionSegments = segmentLoadedCollections(); // for each entity-type, prepare a locking select statement per table. // this is based on the attributes for "state array" ordering purposes - @@ -252,15 +248,17 @@ private QueryOptions buildLockingOptions(ExecutionContext executionContext) { // Used by Hibernate Reactive protected Map> segmentLoadedValues() { final Map> map = new IdentityHashMap<>(); - LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map ); - LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map ); + LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedEntities(), map ); return map; } // Used by Hibernate Reactive protected Map>> segmentLoadedCollections() { + if ( lockScope == Locking.Scope.ROOT_ONLY ) { + return emptyMap(); + } final Map>> map = new HashMap<>(); - LockingHelper.segmentLoadedCollections( loadedValuesCollector.getCollectedCollections(), map ); + LockingHelper.segmentLoadedCollections( loadedValuesCollector.getCollectedCollections(), lockScope, map ); return map; } @@ -286,51 +284,27 @@ protected TableLock createTableLock(TableDetails tableDetails, EntityMappingType protected static LoadedValuesCollectorImpl resolveLoadedValuesCollector( FromClause fromClause, LockingClauseStrategy lockingClauseStrategy) { - final var fromClauseRoots = fromClause.getRoots(); - if ( fromClauseRoots.size() == 1 ) { - return new LoadedValuesCollectorImpl( - List.of( fromClauseRoots.get( 0 ).getNavigablePath() ), - lockingClauseStrategy - ); - } - else { - return new LoadedValuesCollectorImpl( - fromClauseRoots.stream().map( TableGroup::getNavigablePath ).toList(), - lockingClauseStrategy - ); - } + return new LoadedValuesCollectorImpl( lockingClauseStrategy ); } public static class LoadedValuesCollectorImpl implements LoadedValuesCollector { - private final List rootPaths; private final Collection pathsToLock; - private List rootEntitiesToLock; - private List nonRootEntitiesToLock; + private List entitiesToLock; private List collectionsToLock; - public LoadedValuesCollectorImpl(List rootPaths, LockingClauseStrategy lockingClauseStrategy) { - this.rootPaths = rootPaths; + public LoadedValuesCollectorImpl(LockingClauseStrategy lockingClauseStrategy) { pathsToLock = LockingHelper.extractPathsToLock( lockingClauseStrategy ); } @Override public void registerEntity(NavigablePath navigablePath, EntityMappingType entityDescriptor, EntityKey entityKey) { if ( pathsToLock.contains( navigablePath ) ) { - if ( rootPaths.contains( navigablePath ) ) { - if ( rootEntitiesToLock == null ) { - rootEntitiesToLock = new ArrayList<>(); - } - rootEntitiesToLock.add( - new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); - } - else { - if ( nonRootEntitiesToLock == null ) { - nonRootEntitiesToLock = new ArrayList<>(); - } - nonRootEntitiesToLock.add( - new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); + if ( entitiesToLock == null ) { + entitiesToLock = new ArrayList<>(); } + entitiesToLock.add( + new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); } } @@ -347,11 +321,8 @@ public void registerCollection(NavigablePath navigablePath, PluralAttributeMappi @Override public void clear() { - if ( rootEntitiesToLock != null ) { - rootEntitiesToLock.clear(); - } - if ( nonRootEntitiesToLock != null ) { - nonRootEntitiesToLock.clear(); + if ( entitiesToLock != null ) { + entitiesToLock.clear(); } if ( collectionsToLock != null ) { collectionsToLock.clear(); @@ -359,13 +330,8 @@ public void clear() { } @Override - public List getCollectedRootEntities() { - return rootEntitiesToLock; - } - - @Override - public List getCollectedNonRootEntities() { - return nonRootEntitiesToLock; + public List getCollectedEntities() { + return entitiesToLock; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingHelper.java index d483c0245eb7..9e85cd01474c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingHelper.java @@ -6,6 +6,7 @@ import jakarta.persistence.Timeout; import org.hibernate.LockMode; +import org.hibernate.Locking; import org.hibernate.ScrollMode; import org.hibernate.Session; import org.hibernate.collection.spi.PersistentCollection; @@ -43,7 +44,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -84,13 +84,14 @@ public static void lockCollectionTable( final var tableReference = new NamedTableReference( keyTableName, "tbl" ); - querySpec.getFromClause() - .addRoot( new LockingTableGroup( - tableReference, - keyTableName, - attributeMapping, - keyDescriptor.getKeySide().getModelPart() - ) ); + final LockingTableGroup tableGroup = new LockingTableGroup( + tableReference, + keyTableName, + attributeMapping, + keyDescriptor.getKeySide().getModelPart() + ); + querySpec.getFromClause().addRoot( tableGroup ); + querySpec.applyRootPathForLocking( tableGroup.getNavigablePath() ); final var keyPart = keyDescriptor.getKeyPart(); final var columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) ); @@ -169,13 +170,14 @@ public static void lockCollectionTable( final var tableReference = new NamedTableReference( keyTableName, "tbl" ); - querySpec.getFromClause() - .addRoot( new LockingTableGroup( - tableReference, - keyTableName, - attributeMapping, - keyDescriptor.getKeySide().getModelPart() - ) ); + final LockingTableGroup tableGroup = new LockingTableGroup( + tableReference, + keyTableName, + attributeMapping, + keyDescriptor.getKeySide().getModelPart() + ); + querySpec.getFromClause().addRoot( tableGroup ); + querySpec.applyRootPathForLocking( tableGroup.getNavigablePath() ); final var keyPart = keyDescriptor.getKeyPart(); final var columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) ); @@ -325,8 +327,6 @@ public static void lockCollectionTable( SQL_EXEC_LOGGER.followOnLockingForCollectionTable( keyTableName, attributeMapping.getRootPathName() ); } - final var querySpec = new QuerySpec( true ); - final var tableReference = new NamedTableReference( keyTableName, "tbl" ); final var tableGroup = new LockingTableGroup( tableReference, @@ -335,7 +335,9 @@ public static void lockCollectionTable( keyDescriptor.getKeySide().getModelPart() ); + final var querySpec = new QuerySpec( true ); querySpec.getFromClause().addRoot( tableGroup ); + querySpec.applyRootPathForLocking( tableGroup.getNavigablePath() ); final var keyPart = keyDescriptor.getKeyPart(); final var columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) ); @@ -465,20 +467,13 @@ private static void performLocking( public static void logLoadedValues(LoadedValuesCollector collector) { if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { var summary = new StringBuilder(); - summary.append( " Loaded root entities:\n" ); - collector.getCollectedRootEntities().forEach( (reg) -> { + summary.append( " Loaded entities:\n" ); + collector.getCollectedEntities().forEach( (reg) -> { summary.append( String.format( " - %s#%s\n", reg.entityDescriptor().getEntityName(), reg.entityKey().getIdentifier() ) ); } ); - summary.append( " Loaded non-root entities:\n" ); - collector.getCollectedNonRootEntities().forEach( (reg) -> { - summary.append( String.format( " - %s#%s\n", - reg.entityDescriptor().getEntityName() - , reg.entityKey().getIdentifier() ) ); - } ); - summary.append( " Loaded collections:\n" ); collector.getCollectedCollections().forEach( (reg) -> { summary.append( String.format( " - %s#%s\n", @@ -494,29 +489,7 @@ public static void logLoadedValues(LoadedValuesCollector collector) { * from the {@linkplain SqlAstTranslator SQL AST translator}. */ public static Collection extractPathsToLock(LockingClauseStrategy lockingClauseStrategy) { - final LinkedHashSet paths = new LinkedHashSet<>(); - - final var rootsToLock = lockingClauseStrategy.getRootsToLock(); - if ( rootsToLock != null ) { - rootsToLock.forEach( (tableGroup) -> paths.add( tableGroup.getNavigablePath() ) ); - } - - final var joinsToLock = lockingClauseStrategy.getJoinsToLock(); - if ( joinsToLock != null ) { - joinsToLock.forEach( (tableGroupJoin) -> { - final var navigablePath = tableGroupJoin.getNavigablePath(); - paths.add( navigablePath ); - final var modelPart = tableGroupJoin.getJoinedGroup().getModelPart(); - if ( modelPart instanceof PluralAttributeMapping pluralAttributeMapping ) { - paths.add( navigablePath.append( pluralAttributeMapping.getElementDescriptor().getPartName() ) ); - final var indexDescriptor = pluralAttributeMapping.getIndexDescriptor(); - if ( indexDescriptor != null ) { - paths.add( navigablePath.append( indexDescriptor.getPartName() ) ); - } - } - } ); - } - return paths; + return lockingClauseStrategy.getPathsToLock(); } public static void segmentLoadedValues(List registrations, Map> map) { @@ -530,17 +503,23 @@ public static void segmentLoadedValues(List registrations, Map>> map) { + public static void segmentLoadedCollections( + List registrations, + Locking.Scope lockScope, + Map>> map) { if ( registrations != null ) { registrations.forEach( (registration) -> { final var pluralAttributeMapping = registration.collectionDescriptor(); - if ( pluralAttributeMapping.getSeparateCollectionTable() != null ) { - final var attributeKeys = - map.computeIfAbsent( pluralAttributeMapping.findContainingEntityMapping(), - entityMappingType -> new HashMap<>() ); - final var collectionKeys = - attributeKeys.computeIfAbsent( pluralAttributeMapping, - entityMappingType -> new ArrayList<>() ); + if ( lockScope == Locking.Scope.INCLUDE_FETCHES + || pluralAttributeMapping.getSeparateCollectionTable() != null ) { + final var attributeKeys = map.computeIfAbsent( + pluralAttributeMapping.findContainingEntityMapping(), + entityMappingType -> new HashMap<>() + ); + final var collectionKeys = attributeKeys.computeIfAbsent( + pluralAttributeMapping, + entityMappingType -> new ArrayList<>() + ); collectionKeys.add( registration.collectionKey() ); } } ); @@ -557,4 +536,5 @@ public static Map resolveEntityKeys(List entit } ); return map; } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableLock.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableLock.java index 23d594fb8ff4..a7914aa936db 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableLock.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableLock.java @@ -122,6 +122,7 @@ public TableLock( } querySpec.getFromClause().addRoot( physicalTableGroup ); + querySpec.applyRootPathForLocking( rootPath ); creationStates = new LockingCreationStates( querySpec, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectWithActionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectWithActionsBuilder.java index 871b9c98d4cc..6d01cf5a9059 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectWithActionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectWithActionsBuilder.java @@ -11,25 +11,37 @@ import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.tree.select.QuerySpec; +/// Contract used while building a [JdbcSelect] which might potentially +/// include [pre-][PreAction] and/or [post-][PostAction] actions. +/// +/// @author Steve Ebersole +/// @author Andrea Boriero @Incubating // Used by Hibernate Reactive public interface JdbcSelectWithActionsBuilder { - + /// The primary selection. JdbcSelectWithActionsBuilder setPrimaryAction(JdbcSelect primaryAction); + /// Collector of loaded values for post-processing. JdbcSelectWithActionsBuilder setLoadedValuesCollector(LoadedValuesCollector loadedValuesCollector); + /// Lock-timeout handling type. JdbcSelectWithActionsBuilder setLockTimeoutType(LockTimeoutType lockTimeoutType); + /// Dialect's support for locking. JdbcSelectWithActionsBuilder setLockingSupport(LockingSupport lockingSupport); + /// Requested lock options. JdbcSelectWithActionsBuilder setLockOptions(LockOptions lockOptions); + /// QuerySpec (selection) which is the target of locking. JdbcSelectWithActionsBuilder setLockingTarget(QuerySpec lockingTarget); + /// Access to locking details - used for paths to lock, mainly. JdbcSelectWithActionsBuilder setLockingClauseStrategy(LockingClauseStrategy lockingClauseStrategy); - JdbcSelectWithActionsBuilder setIsFollowOnLockStrategy(boolean isFollonOnLockStrategy); + /// Whether follow-on locking should be used. + JdbcSelectWithActionsBuilder setIsFollowOnLockStrategy(boolean isFollowOnLockStrategy); JdbcSelectWithActionsBuilder appendPreAction(PreAction... actions); @@ -43,6 +55,6 @@ public interface JdbcSelectWithActionsBuilder { JdbcSelectWithActionsBuilder addSecondaryActionPair(PreAction preAction, PostAction postAction); + /// Build the appropriate JdbcSelect. JdbcSelect build(); - } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/LoadedValuesCollector.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/LoadedValuesCollector.java index 054515b97b98..613a355ea5d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/LoadedValuesCollector.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/LoadedValuesCollector.java @@ -57,12 +57,7 @@ void registerCollection( /** * Access to all root entities loaded. */ - List getCollectedRootEntities(); - - /** - * Access to all non-root entities (join fetches e.g.) loaded. - */ - List getCollectedNonRootEntities(); + List getCollectedEntities(); /** * Access to all collection loaded. diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/LockingBasedOnSelectClauseTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/LockingBasedOnSelectClauseTests.java new file mode 100644 index 000000000000..6d08cc2dc872 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/LockingBasedOnSelectClauseTests.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.LockModeType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.transaction.TransactionUtil; +import org.hibernate.testing.util.ast.HqlHelper; +import org.hibernate.testing.util.ast.LoadingAstHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@JiraKey("HHH-19925") +@DomainModel(annotatedClasses = { + LockingBasedOnSelectClauseTests.Book.class, + LockingBasedOnSelectClauseTests.Author.class +}) +@SessionFactory +public class LockingBasedOnSelectClauseTests { + @BeforeAll + void setUp(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var king = new Author( 1, "Stephen King" ); + var darkTower = new Book( 1, "The Dark Tower", king ); + session.persist( king ); + session.persist( darkTower ); + } ); + } + + @AfterAll + void tearDown(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + + @Test + @SkipForDialect(dialectClass = HSQLDialect.class, reason = "See https://sourceforge.net/p/hsqldb/bugs/1734/") + void testBasicHqlUsage(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.createQuery( "select b.author from Book b" ) + .setLockMode( LockModeType.PESSIMISTIC_WRITE ) + .list(); + // The correct outcome here is for the authors table to be locked as the root selection. + TransactionUtil.assertRowLock( + factoryScope, + "authors", + "name", + "id", + 1, + true + ); + } ); + } + + @Test + void testSubQueryHqlTranslation(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Book b where b.id in (select id from Book)" ).list(); + } ); + } + + private static final String BOOK_PATH = "org.hibernate.orm.test.locking.LockingBasedOnSelectClauseTests$Book"; + private static final String BOOK_PATH_HQL = BOOK_PATH+ "(b)"; + private static final String BOOK_AUTHOR_PATH_HQL = BOOK_PATH_HQL + ".author"; + + @Test + void testBasicHqlTranslation(SessionFactoryScope factoryScope) { + final HqlHelper.HqlTranslation hqlTranslation = HqlHelper.translateHql( + "select b.author from Book b", + factoryScope.getSessionFactory() + ); + + final Statement sqlAst = hqlTranslation.sqlAst(); + assertThat( sqlAst ).isInstanceOf( SelectStatement.class ); + final SelectStatement selectAst = ( SelectStatement ) sqlAst; + assertThat( selectAst.getQuerySpec().getRootPathsForLocking() ).hasSize( 1 ); + assertThat( selectAst.getQuerySpec().getRootPathsForLocking().iterator().next().getFullPath() ) + .isEqualTo( BOOK_AUTHOR_PATH_HQL ); + } + + @Test + void testScalarHqlTranslation(SessionFactoryScope factoryScope) { + final HqlHelper.HqlTranslation hqlTranslation = HqlHelper.translateHql( + "select b.title from Book b", + factoryScope.getSessionFactory() + ); + + final Statement sqlAst = hqlTranslation.sqlAst(); + assertThat( sqlAst ).isInstanceOf( SelectStatement.class ); + final SelectStatement selectAst = ( SelectStatement ) sqlAst; + assertThat( selectAst.getQuerySpec().getRootPathsForLocking() ).hasSize( 1 ); + assertThat( selectAst.getQuerySpec().getRootPathsForLocking().iterator().next().getFullPath() ) + .isEqualTo( BOOK_PATH_HQL ); + } + + @Test + void testScalarHqlTranslation2(SessionFactoryScope factoryScope) { + final HqlHelper.HqlTranslation hqlTranslation = HqlHelper.translateHql( + "select b.title, b.author from Book b", + factoryScope.getSessionFactory() + ); + + final Statement sqlAst = hqlTranslation.sqlAst(); + assertThat( sqlAst ).isInstanceOf( SelectStatement.class ); + final SelectStatement selectAst = ( SelectStatement ) sqlAst; + assertThat( selectAst.getQuerySpec().getRootPathsForLocking() ).hasSize( 2 ); + final Iterator paths = selectAst.getQuerySpec().getRootPathsForLocking().iterator(); + assertThat( paths.next().getFullPath() ).isEqualTo( BOOK_PATH_HQL ); + assertThat( paths.next().getFullPath() ).isEqualTo( BOOK_AUTHOR_PATH_HQL ); + } + + @Test + void testDynamicInstantiationHqlTranslation(SessionFactoryScope factoryScope) { + final HqlHelper.HqlTranslation hqlTranslation = HqlHelper.translateHql( + "select new list(b.title, b.author) from Book b", + factoryScope.getSessionFactory() + ); + + final Statement sqlAst = hqlTranslation.sqlAst(); + assertThat( sqlAst ).isInstanceOf( SelectStatement.class ); + final SelectStatement selectAst = ( SelectStatement ) sqlAst; + assertThat( selectAst.getQuerySpec().getRootPathsForLocking() ).hasSize( 2 ); + final Iterator paths = selectAst.getQuerySpec().getRootPathsForLocking().iterator(); + assertThat( paths.next().getFullPath() ).isEqualTo( BOOK_PATH_HQL ); + assertThat( paths.next().getFullPath() ).isEqualTo( BOOK_AUTHOR_PATH_HQL ); + } + + @Test + void testLoadingTranslation(SessionFactoryScope factoryScope) { + var entityDescriptor = factoryScope.getSessionFactory().getMappingMetamodel().getEntityDescriptor( Book.class ); + var translation = LoadingAstHelper.translateLoading( + entityDescriptor, + 1, + factoryScope.getSessionFactory() + ); + assertThat( translation.sqlAst().getQuerySpec().getRootPathsForLocking() ).hasSize( 1 ); + assertThat( translation.sqlAst().getQuerySpec().getRootPathsForLocking().iterator().next().getFullPath() ) + .isEqualTo( BOOK_PATH ); + } + + @Entity(name="Book") + @Table(name="books") + public static class Book { + @Id + private Integer id; + private String title; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_fk") + private Author author; + + public Book() { + } + + public Book(Integer id, String title, Author author) { + this.id = id; + this.title = title; + this.author = author; + } + } + + @Entity(name="Author") + @Table(name="authors") + public static class Author { + @Id + private Integer id; + private String name; + + public Author() { + } + + public Author(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ScopeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ScopeTests.java index 762b22e6d4c8..83d9481612cc 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ScopeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ScopeTests.java @@ -6,7 +6,6 @@ import org.hibernate.EnabledFetchProfile; import org.hibernate.Hibernate; -import org.hibernate.Locking; import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.H2Dialect; @@ -27,6 +26,7 @@ import static jakarta.persistence.PessimisticLockScope.EXTENDED; import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.LockMode.PESSIMISTIC_WRITE; +import static org.hibernate.Locking.Scope.INCLUDE_FETCHES; import static org.hibernate.orm.test.locking.options.Helper.Table.BOOKS; import static org.hibernate.orm.test.locking.options.Helper.Table.BOOK_AUTHORS; import static org.hibernate.orm.test.locking.options.Helper.Table.BOOK_GENRES; @@ -209,7 +209,7 @@ private boolean willAggressivelyLockJoinedTables(Dialect dialect) { @SkipForDialect(dialectClass = InformixDialect.class, reason = "Cursor must be on simple SELECT for FOR UPDATE") void testEagerFindWithFetchScope(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES ); + final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, INCLUDE_FETCHES ); REPORTS.checkLocked( report.getId(), true, factoryScope ); PERSONS.checkLocked( report.getReporter().getId(), true, factoryScope ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/util/ast/HqlHelper.java b/hibernate-testing/src/main/java/org/hibernate/testing/util/ast/HqlHelper.java new file mode 100644 index 000000000000..e3f93cd99048 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/util/ast/HqlHelper.java @@ -0,0 +1,357 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.util.ast; + +import org.hibernate.Session; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.query.ParameterMetadata; +import org.hibernate.query.hql.HqlTranslator; +import org.hibernate.query.internal.ParameterMetadataImpl; +import org.hibernate.query.internal.QueryParameterBindingsImpl; +import org.hibernate.query.spi.HqlInterpretation; +import org.hibernate.query.spi.ParameterMetadataImplementor; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBinding; +import org.hibernate.query.spi.QueryParameterImplementor; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.sql.SqmTranslator; +import org.hibernate.query.sqm.sql.SqmTranslatorFactory; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.insert.InsertStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryDelete; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQueryUpdate; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +/// Utilities for helping test HQL translation +/// +/// @author Steve Ebersole +public class HqlHelper { + + /// Translation details about a particular HQL + /// + /// @param hql The translated HQL + /// @param sqm The corresponding SQM AST + /// @param sql The corresponding SQL + /// @param sqlAst The corresponding SQL AST + /// @param parameterMetadata Details about any query parameters + public record HqlTranslation( + String hql, + SqmStatement sqm, + String sql, + Statement sqlAst, + ParameterMetadata parameterMetadata) { + } + + /// Performs the translation, returning the details. Delegates to {@linkplain #translateHql(String, Class, SessionFactoryImplementor)} + /// passing {@code Object[]} as the expected result type. + public static HqlTranslation translateHql(String hql, SessionFactoryImplementor sessionFactory) { + return translateHql( hql, Object[].class, sessionFactory ); + } + + /// Performs the translation, returning the details. + @SuppressWarnings("rawtypes") + public static HqlTranslation translateHql(String hql, Class resultType, SessionFactoryImplementor sessionFactory) { + final HqlTranslator hqlTranslator = sessionFactory.getQueryEngine().getHqlTranslator(); + final SqmStatement sqmAst = hqlTranslator.translate( hql, resultType ); + + if ( sqmAst instanceof SqmSelectStatement sqmSelect ) { + //noinspection unchecked + return new SqmSelectInterpreter<>( hql, sessionFactory ).interpret( sqmSelect, sessionFactory ); + } + else if ( sqmAst instanceof SqmDeleteStatement sqmDelete ) { + //noinspection unchecked + return new SqmDeleteInterpreter<>( hql, sessionFactory ).interpret( sqmDelete, sessionFactory ); + } + else if ( sqmAst instanceof SqmUpdateStatement sqmUpdate ) { + //noinspection unchecked + return new SqmUpdateInterpreter<>( hql, sessionFactory ).interpret( sqmUpdate, sessionFactory ); + } + else if ( sqmAst instanceof SqmInsertStatement sqmInsert ) { + //noinspection unchecked + return new SqmInsertInterpreter<>( hql, sessionFactory ).interpret( sqmInsert, sessionFactory ); + } + + throw new UnsupportedOperationException( "Unexpected SQM type from HQL - " + sqmAst.getClass().getName() ); + } + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // internals + + private static abstract class SqmInterpreter, S extends Statement, J extends JdbcOperation> { + protected final String hql; + protected final SessionFactoryImplementor sessionFactory; + + public SqmInterpreter(String hql, SessionFactoryImplementor sessionFactory) { + this.hql = hql; + this.sessionFactory = sessionFactory; + } + + public HqlTranslation interpret(T sqmAst, SessionFactoryImplementor sessionFactory) { + final HqlInterpretation hqlInterpretation = createHqlInterpretation( sqmAst ); + + final QueryParameterBindingsImpl domainParameterBindings = QueryParameterBindingsImpl.from( + hqlInterpretation.getParameterMetadata(), + sessionFactory + ); + + final SqmTranslator sqmTranslator = createSqmTranslator( hqlInterpretation, domainParameterBindings ); + final SqmTranslation sqmTranslation = sqmTranslator.translate(); + + final SqlAstTranslator sqlAstTranslator = createSqlAstTranslator( sqmTranslation ); + + final J jdbcOperation = sessionFactory.fromSession( (session) -> { + final JdbcParameterBindings jdbcParameterBindings = createJdbcParameterBindings( + sqmTranslation, + hqlInterpretation.getDomainParameterXref(), + domainParameterBindings, + session + ); + return sqlAstTranslator.translate( jdbcParameterBindings, QueryOptions.NONE ); + } ); + + return new HqlTranslation( + hql, + hqlInterpretation.getSqmStatement(), + jdbcOperation.getSqlString(), + sqmTranslation.getSqlAst(), + hqlInterpretation.getParameterMetadata() + ); + } + + private HqlInterpretation createHqlInterpretation(T sqmAst) { + final ParameterMetadataImplementor parameterMetadata; + final DomainParameterXref domainParameterXref; + + if ( sqmAst.getSqmParameters().isEmpty() ) { + domainParameterXref = DomainParameterXref.EMPTY; + parameterMetadata = ParameterMetadataImpl.EMPTY; + } + else { + domainParameterXref = DomainParameterXref.from( sqmAst ); + parameterMetadata = new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ); + } + + return new NonCopyingHqlInterpretationImpl<>( sqmAst, parameterMetadata, domainParameterXref ); + } + + protected abstract SqmTranslator createSqmTranslator( + HqlInterpretation hqlInterpretation, + QueryParameterBindingsImpl parameterBindings); + + protected abstract SqlAstTranslator createSqlAstTranslator( + SqmTranslation sqmTranslation); + + private JdbcParameterBindings createJdbcParameterBindings( + SqmTranslation sqmTranslation, + DomainParameterXref domainParameterXref, + QueryParameterBindingsImpl parameterBindings, + Session session) { + return SqmUtil.createJdbcParameterBindings( + parameterBindings, + domainParameterXref, + SqmUtil.generateJdbcParamsXref( + domainParameterXref, + sqmTranslation::getJdbcParamsBySqmParam + ), + new SqmParameterMappingModelResolutionAccess() { + @Override + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + final QueryParameterImplementor domainParam = domainParameterXref.getQueryParameter( parameter ); + final QueryParameterBinding binding = parameterBindings.getBinding( domainParam ); + //noinspection unchecked + return (MappingModelExpressible) binding.getType(); + } + }, + session.unwrap( SharedSessionContractImplementor.class ) + ); + } + } + + private static class SqmSelectInterpreter extends SqmInterpreter, SelectStatement, JdbcOperationQuerySelect> { + public SqmSelectInterpreter( + String hql, + SessionFactoryImplementor sessionFactory) { + super( hql, sessionFactory ); + } + + @Override + protected SqmTranslator createSqmTranslator( + HqlInterpretation hqlInterpretation, + QueryParameterBindingsImpl parameterBindings) { + final SqmTranslatorFactory sqmTranslatorFactory = sessionFactory.getQueryEngine().getSqmTranslatorFactory(); + return sqmTranslatorFactory.createSelectTranslator( + (SqmSelectStatement) hqlInterpretation.getSqmStatement(), + QueryOptions.NONE, + hqlInterpretation.getDomainParameterXref(), + parameterBindings, + new LoadQueryInfluencers( sessionFactory), + sessionFactory.getSqlTranslationEngine(), + true + ); + } + + @Override + protected SqlAstTranslator createSqlAstTranslator( + SqmTranslation sqmTranslation) { + final SqlAstTranslatorFactory sqlAstTranslatorFactory = sessionFactory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory(); + return sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory, sqmTranslation.getSqlAst() ); + } + } + + private static class SqmDeleteInterpreter extends SqmInterpreter, DeleteStatement, JdbcOperationQueryDelete> { + public SqmDeleteInterpreter(String hql, SessionFactoryImplementor sessionFactory) { + super( hql, sessionFactory ); + } + + @Override + protected SqmTranslator createSqmTranslator( + HqlInterpretation hqlInterpretation, + QueryParameterBindingsImpl parameterBindings) { + final SqmTranslatorFactory sqmTranslatorFactory = sessionFactory.getQueryEngine().getSqmTranslatorFactory(); + //noinspection unchecked + return (SqmTranslator) sqmTranslatorFactory.createMutationTranslator( + (SqmDeleteStatement) hqlInterpretation.getSqmStatement(), + QueryOptions.NONE, + hqlInterpretation.getDomainParameterXref(), + parameterBindings, + new LoadQueryInfluencers(sessionFactory), + sessionFactory.getSqlTranslationEngine() + ); + } + + @Override + protected SqlAstTranslator createSqlAstTranslator(SqmTranslation sqmTranslation) { + final SqlAstTranslatorFactory sqlAstTranslatorFactory = sessionFactory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory(); + //noinspection unchecked + return (SqlAstTranslator) sqlAstTranslatorFactory.buildMutationTranslator( + sessionFactory, + sqmTranslation.getSqlAst() + ); + } + } + + private static class SqmUpdateInterpreter extends SqmInterpreter, UpdateStatement, JdbcOperationQueryUpdate> { + public SqmUpdateInterpreter(String hql, SessionFactoryImplementor sessionFactory) { + super( hql, sessionFactory ); + } + + @Override + protected SqmTranslator createSqmTranslator( + HqlInterpretation hqlInterpretation, + QueryParameterBindingsImpl parameterBindings) { + final SqmTranslatorFactory sqmTranslatorFactory = sessionFactory.getQueryEngine().getSqmTranslatorFactory(); + //noinspection unchecked + return (SqmTranslator) sqmTranslatorFactory.createMutationTranslator( + (SqmUpdateStatement) hqlInterpretation.getSqmStatement(), + QueryOptions.NONE, + hqlInterpretation.getDomainParameterXref(), + parameterBindings, + new LoadQueryInfluencers(sessionFactory), + sessionFactory.getSqlTranslationEngine() + ); + } + + @Override + protected SqlAstTranslator createSqlAstTranslator(SqmTranslation sqmTranslation) { + final SqlAstTranslatorFactory sqlAstTranslatorFactory = sessionFactory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory(); + //noinspection unchecked + return (SqlAstTranslator) sqlAstTranslatorFactory.buildMutationTranslator( + sessionFactory, + sqmTranslation.getSqlAst() + ); + } + } + + private static class SqmInsertInterpreter extends SqmInterpreter, InsertStatement, JdbcOperationQueryInsert> { + public SqmInsertInterpreter(String hql, SessionFactoryImplementor sessionFactory) { + super( hql, sessionFactory ); + } + + @Override + protected SqmTranslator createSqmTranslator( + HqlInterpretation hqlInterpretation, + QueryParameterBindingsImpl parameterBindings) { + final SqmTranslatorFactory sqmTranslatorFactory = sessionFactory.getQueryEngine().getSqmTranslatorFactory(); + //noinspection unchecked + return (SqmTranslator) sqmTranslatorFactory.createMutationTranslator( + (SqmInsertStatement) hqlInterpretation.getSqmStatement(), + QueryOptions.NONE, + hqlInterpretation.getDomainParameterXref(), + parameterBindings, + new LoadQueryInfluencers(sessionFactory), + sessionFactory.getSqlTranslationEngine() + ); + } + + @Override + protected SqlAstTranslator createSqlAstTranslator(SqmTranslation sqmTranslation) { + final SqlAstTranslatorFactory sqlAstTranslatorFactory = sessionFactory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory(); + //noinspection unchecked + return (SqlAstTranslator) sqlAstTranslatorFactory.buildMutationTranslator( + sessionFactory, + sqmTranslation.getSqlAst() + ); + } + } + + private record NonCopyingHqlInterpretationImpl( + SqmStatement sqmAst, + ParameterMetadataImplementor parameterMetadata, + DomainParameterXref domainParameterXref) implements HqlInterpretation { + @Override + public SqmStatement getSqmStatement() { + return sqmAst(); + } + + @Override + public ParameterMetadataImplementor getParameterMetadata() { + return parameterMetadata(); + } + + @Override + public DomainParameterXref getDomainParameterXref() { + return domainParameterXref(); + } + + @Override + public void validateResultType(Class resultType) { + // irrelevant here + } + } + +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/util/ast/LoadingAstHelper.java b/hibernate-testing/src/main/java/org/hibernate/testing/util/ast/LoadingAstHelper.java new file mode 100644 index 000000000000..5d7769923b00 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/util/ast/LoadingAstHelper.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.util.ast; + +import org.hibernate.LockOptions; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.loader.ast.internal.LoaderSelectBuilder; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Steve Ebersole + */ +public class LoadingAstHelper { + /// Translation details for loading + /// + /// @param sql The corresponding SQL + /// @param sqlAst The corresponding SQL AST + /// @param jdbcParameters The corresponding JDBC parameters + public record LoaderTranslation( + String sql, + SelectStatement sqlAst, + List jdbcParameters) { + } + + public static LoaderTranslation translateLoading( + EntityMappingType entityMappingType, + I id, + SessionFactoryImplementor sessionFactory) { + return translateLoading( entityMappingType, List.of(id), sessionFactory ); + } + + public static LoaderTranslation translateLoading( + EntityMappingType entityMappingType, + List ids, + SessionFactoryImplementor sessionFactory) { + var jdbcParameters = new ArrayList(); + var sqlAst = LoaderSelectBuilder.createSelect( + entityMappingType, + null, + entityMappingType.getIdentifierMapping(), + null, + ids.size(), + new LoadQueryInfluencers( sessionFactory ), + LockOptions.NONE, + jdbcParameters::add, + sessionFactory + ); + var sqlAstTranslator = sessionFactory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator( sessionFactory, sqlAst ); + var jdbcOperation = sqlAstTranslator.translate( + buildJdbcParameterBindings( entityMappingType.getIdentifierMapping(), ids, jdbcParameters ), + QueryOptions.NONE + ); + return new LoaderTranslation( jdbcOperation.getSqlString(), sqlAst, jdbcParameters ); + } + + private static JdbcParameterBindings buildJdbcParameterBindings( + EntityIdentifierMapping identifierMapping, + List ids, + ArrayList jdbcParameters) { + final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); + identifierMapping.forEachJdbcType( (position, jdbcMapping) -> jdbcParameterBindings.addBinding( + jdbcParameters.get( position ), + new JdbcParameterBindingImpl( jdbcMapping, null ) + ) ); + return jdbcParameterBindings; + } + +}