diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java index b22e332d047e..2633d0acbd5e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java @@ -48,7 +48,7 @@ import org.hibernate.sql.ast.tree.predicate.PredicateContainer; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.type.BasicType; import org.hibernate.type.SqlTypes; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java index de8419778d8e..2f8924011b27 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java @@ -15,9 +15,15 @@ import java.util.function.Function; /** + * Helper for dealing with {@linkplain Connection}-level lock timeouts. + * * @author Steve Ebersole */ public class Helper { + /** + * Use the given {@code sql} statement to query the current lock-timeout for the + * {@linkplain Connection} and use the {@code extractor} to process the value. + */ public static Timeout getLockTimeout( String sql, TimeoutExtractor extractor, @@ -37,15 +43,13 @@ public static Timeout getLockTimeout( } } + /** + * Set the {@linkplain Connection}-level lock-timeout using the given {@code sql} command. + */ public static void setLockTimeout( - Timeout timeout, - Function valueStrategy, - String sqlFormat, + String sql, Connection connection, SessionFactoryImplementor factory) { - final int milliseconds = valueStrategy.apply( timeout ); - - final String sql = String.format( sqlFormat, milliseconds ); try (final java.sql.Statement statement = connection.createStatement()) { factory.getJdbcServices().getSqlStatementLogger().logStatement( sql ); statement.execute( sql ); @@ -56,6 +60,39 @@ public static void setLockTimeout( } } + /** + * Set the {@linkplain Connection}-level lock-timeout using + * the given {@code sqlFormat} (with a single format placeholder + * for the {@code milliseconds} value). + * + * @see #setLockTimeout(String, Connection, SessionFactoryImplementor) + */ + public static void setLockTimeout( + Integer milliseconds, + String sqlFormat, + Connection connection, + SessionFactoryImplementor factory) { + final String sql = String.format( sqlFormat, milliseconds ); + setLockTimeout( sql, connection, factory ); + } + + /** + * Set the {@linkplain Connection}-level lock-timeout. The passed + * {@code valueStrategy} is used to interpret the {@code timeout} + * which is then used with {@code sqlFormat} to execute the command. + * + * @see #setLockTimeout(Integer, String, Connection, SessionFactoryImplementor) + */ + public static void setLockTimeout( + Timeout timeout, + Function valueStrategy, + String sqlFormat, + Connection connection, + SessionFactoryImplementor factory) { + final int milliseconds = valueStrategy.apply( timeout ); + setLockTimeout( milliseconds, sqlFormat, connection, factory ); + } + @FunctionalInterface public interface TimeoutExtractor { Timeout extractFrom(ResultSet resultSet) throws SQLException; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/OracleLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/OracleLockingSupport.java index 58e3b6d31a40..bc914ad25820 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/OracleLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/OracleLockingSupport.java @@ -61,7 +61,9 @@ public RowLockStrategy getWriteRowLockStrategy() { @Override public OuterJoinLockingType getOuterJoinLockingType() { - return OuterJoinLockingType.UNSUPPORTED; + // Per Loic, as of 23 at least, Oracle does support this. + // Let's see what CI says for previous supported versions. + return OuterJoinLockingType.IDENTIFIED; } @Override 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 68db4f3a38fe..ec06849cb50e 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 @@ -8,6 +8,7 @@ import org.hibernate.LockOptions; import org.hibernate.Locking; import org.hibernate.StaleObjectStateException; +import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.dialect.lock.LockingStrategy; import org.hibernate.dialect.lock.LockingStrategyException; import org.hibernate.dialect.lock.PessimisticEntityLockException; @@ -23,6 +24,7 @@ import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SimpleFromClauseAccessImpl; import org.hibernate.sql.ast.spi.SqlAliasBaseManager; @@ -34,8 +36,9 @@ import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.lock.LockingHelper; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; @@ -157,7 +160,7 @@ public void lock( } final SelectStatement selectStatement = new SelectStatement( rootQuerySpec, List.of( idResult ) ); - final JdbcOperationQuerySelect selectOperation = session + final JdbcSelect selectOperation = session .getDialect() .getSqlAstTranslatorFactory() .buildSelectTranslator( factory, selectStatement ) @@ -176,6 +179,19 @@ public void lock( 1, SingleResultConsumer.instance() ); + + if ( lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS ) { + SqmMutationStrategyHelper.visitCollectionTables( entityToLock, (attribute) -> { + final PersistentCollection collectionToLock = (PersistentCollection) attribute.getValue( object ); + LockingHelper.lockCollectionTable( + attribute, + lockMode, + lockOptions.getTimeout(), + collectionToLock, + lockingExecutionContext + ); + } ); + } } catch (LockTimeoutException e) { throw new PessimisticEntityLockException( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java index e32c12f1fca3..95714c02456d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java @@ -41,17 +41,17 @@ public class TransactSQLLockingSupport extends LockingSupportParameterized { LockTimeoutType.NONE, RowLockStrategy.TABLE, OuterJoinLockingType.IDENTIFIED, - ConnectionLockTimeoutStrategy.NONE + SybaseImpl.IMPL ); public static final LockingSupport SYBASE_ASE = new TransactSQLLockingSupport( - PessimisticLockStyle.NONE, + PessimisticLockStyle.TABLE_HINT, LockTimeoutType.CONNECTION, LockTimeoutType.NONE, LockTimeoutType.NONE, RowLockStrategy.TABLE, OuterJoinLockingType.IDENTIFIED, - ConnectionLockTimeoutStrategy.NONE + SybaseImpl.IMPL ); public static final LockingSupport SYBASE_LEGACY = new TransactSQLLockingSupport( @@ -61,7 +61,7 @@ public class TransactSQLLockingSupport extends LockingSupportParameterized { LockTimeoutType.NONE, RowLockStrategy.TABLE, OuterJoinLockingType.IDENTIFIED, - ConnectionLockTimeoutStrategy.NONE + SybaseImpl.IMPL ); public static LockingSupport forSybaseAnywhere(DatabaseVersion version) { @@ -76,7 +76,7 @@ public static LockingSupport forSybaseAnywhere(DatabaseVersion version) { ? RowLockStrategy.COLUMN : RowLockStrategy.TABLE, OuterJoinLockingType.IDENTIFIED, - ConnectionLockTimeoutStrategy.NONE + SybaseImpl.IMPL ); } @@ -111,13 +111,13 @@ public static class SQLServerImpl implements ConnectionLockTimeoutStrategy { @Override public Level getSupportedLevel() { - return ConnectionLockTimeoutStrategy.Level.EXTENDED; + return Level.EXTENDED; } @Override public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) { return Helper.getLockTimeout( - "select @@LOCK_TIMEOUT", + "select @@lock_timeout", (resultSet) -> { final int timeoutInMilliseconds = resultSet.getInt( 1 ); return switch ( timeoutInMilliseconds ) { @@ -148,4 +148,56 @@ public void setLockTimeout(Timeout timeout, Connection connection, SessionFactor ); } } + + public static class SybaseImpl implements ConnectionLockTimeoutStrategy { + public static final SybaseImpl IMPL = new SybaseImpl(); + + @Override + public Level getSupportedLevel() { + return Level.SUPPORTED; + } + + @Override + public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) { + return Helper.getLockTimeout( + "select @@lock_timeout", + (resultSet) -> { + final int timeoutInMilliseconds = resultSet.getInt( 1 ); + return switch ( timeoutInMilliseconds ) { + case -1 -> Timeouts.WAIT_FOREVER; + case 0 -> Timeouts.NO_WAIT; + default -> Timeout.milliseconds( timeoutInMilliseconds ); + }; + }, + connection, + factory + ); + } + + @Override + public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) { + final int milliseconds = timeout.milliseconds(); + + if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) { + throw new HibernateException( "Sybase does not accept skip-locked for lock-timeout" ); + } + + // Sybase needs a special syntax for NO_WAIT rather than a number + if ( milliseconds == Timeouts.NO_WAIT_MILLI ) { + // NOTE: The docs say this is supported, and it does not fail when used, + // but immediately after the setting value is still -1. So it seems to + // allow the call but ignore it. Might just be jTDS. + Helper.setLockTimeout( "set lock nowait", connection, factory ); + } + else if ( milliseconds == Timeouts.WAIT_FOREVER_MILLI ) { + // Even though Sybase's wait-forever (and default) value is -1, it won't accept + // -1 as a value because, well, of course it won't. Need to set max value instead + // because, well, of course you do. + Helper.setLockTimeout( 2147483647, "set lock wait %s", connection, factory ); + } + else { + Helper.setLockTimeout( milliseconds, "set lock wait %s", connection, factory ); + } + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/OracleSqlAstTranslator.java index 1c561ac2a6aa..e4c6e8ed0eba 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/OracleSqlAstTranslator.java @@ -190,7 +190,9 @@ else if ( followOnStrategy == Locking.FollowOn.IGNORE ) { } } - if ( strategy != LockStrategy.FOLLOW_ON && needsLockingWrapper( querySpec ) && !canApplyLockingWrapper( querySpec ) ) { + if ( strategy != LockStrategy.FOLLOW_ON + && needsLockingWrapper( querySpec, followOnStrategy ) + && !canApplyLockingWrapper( querySpec ) ) { if ( followOnStrategy == Locking.FollowOn.DISALLOW ) { throw new IllegalQueryOperationException( "Locking with OFFSET/FETCH is not supported" ); } @@ -317,7 +319,11 @@ public void visitQueryGroup(QueryGroup queryGroup) { @Override public void visitQuerySpec(QuerySpec querySpec) { - final EntityIdentifierMapping identifierMappingForLockingWrapper = identifierMappingForLockingWrapper( querySpec ); + final LockOptions lockOptions = getLockOptions(); + final Locking.FollowOn followOnStrategy = lockOptions == null + ? Locking.FollowOn.ALLOW + : lockOptions.getFollowOnStrategy(); + final EntityIdentifierMapping identifierMappingForLockingWrapper = identifierMappingForLockingWrapper( querySpec, followOnStrategy ); final Expression offsetExpression; final Expression fetchExpression; final FetchClauseType fetchClauseType; @@ -423,13 +429,13 @@ private QuerySpec createLockingWrapper( return lockingWrapper; } - private EntityIdentifierMapping identifierMappingForLockingWrapper(QuerySpec querySpec) { + private EntityIdentifierMapping identifierMappingForLockingWrapper(QuerySpec querySpec, Locking.FollowOn followOnStrategy) { // We only need a locking wrapper for very simple queries if ( canApplyLockingWrapper( querySpec ) // There must be the need for locking in this query && needsLocking( querySpec ) // The query uses some sort of pagination which makes the wrapper necessary - && needsLockingWrapper( querySpec ) + && needsLockingWrapper( querySpec, followOnStrategy ) // The query may not have a group by, having and distinct clause, or use aggregate functions, // as these features will force the use of follow-on locking && querySpec.getGroupByClauseExpressions().isEmpty() @@ -450,9 +456,8 @@ private boolean canApplyLockingWrapper(QuerySpec querySpec) { && fromClause.getRoots().get( 0 ).getModelPart() instanceof EntityMappingType; } - private boolean needsLockingWrapper(QuerySpec querySpec) { - final LockOptions lockOptions = getLockOptions(); - if ( lockOptions.getFollowOnStrategy() == Locking.FollowOn.FORCE ) { + private boolean needsLockingWrapper(QuerySpec querySpec, Locking.FollowOn followOnStrategy) { + if ( followOnStrategy == Locking.FollowOn.FORCE ) { // user explicitly asked for follow-on locking return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java index b39816dfea54..62c902b1f196 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java @@ -229,9 +229,6 @@ public static String determineLockHint(LockMode lockMode, int effectiveLockTimeo protected LockStrategy determineLockingStrategy( QuerySpec querySpec, Locking.FollowOn followOnStrategy) { - if ( followOnStrategy == Locking.FollowOn.FORCE ) { - return LockStrategy.FOLLOW_ON; - } // No need for follow on locking return LockStrategy.CLAUSE; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java index a07c80809d46..7afd01da5c41 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java @@ -187,9 +187,6 @@ public static String determineLockHint(LockMode lockMode) { protected LockStrategy determineLockingStrategy( QuerySpec querySpec, Locking.FollowOn followOnStrategy) { - if ( followOnStrategy == Locking.FollowOn.FORCE ) { - return LockStrategy.FOLLOW_ON; - } // No need for follow on locking return LockStrategy.CLAUSE; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcServices.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcServices.java index a3b5d1a73ab1..dfaebc03c82e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcServices.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcServices.java @@ -17,7 +17,7 @@ import org.hibernate.sql.exec.internal.StandardJdbcMutationExecutor; import org.hibernate.sql.exec.spi.JdbcMutationExecutor; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; /** diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryDelegatingImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryDelegatingImpl.java index 6544763a5af5..1ce6152abe87 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryDelegatingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryDelegatingImpl.java @@ -4,15 +4,6 @@ */ package org.hibernate.engine.spi; -import java.sql.Connection; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.naming.NamingException; -import javax.naming.Reference; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceUnitTransactionType; @@ -21,7 +12,6 @@ import jakarta.persistence.SynchronizationType; import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQueryReference; - import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.HibernateException; import org.hibernate.Session; @@ -37,12 +27,12 @@ import org.hibernate.engine.creation.spi.SessionBuilderImplementor; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.profile.FetchProfile; +import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EntityCopyObserverFactory; import org.hibernate.event.spi.EventEngine; -import org.hibernate.graph.RootGraph; +import org.hibernate.generator.Generator; import org.hibernate.graph.spi.RootGraphImplementor; -import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.model.domain.JpaMetamodel; import org.hibernate.metamodel.spi.RuntimeMetamodelsImplementor; @@ -56,11 +46,20 @@ import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; import org.hibernate.stat.spi.StatisticsImplementor; -import org.hibernate.generator.Generator; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.spi.TypeConfiguration; +import javax.naming.NamingException; +import javax.naming.Reference; +import java.sql.Connection; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + /** * Base delegating implementation of the {@link SessionFactory} and * {@link SessionFactoryImplementor} contracts for intended for easier @@ -230,7 +229,7 @@ public SqlStringGenerationContext getSqlStringGenerationContext() { } @Override - public RootGraph> createGraphForDynamicEntity(String entityName) { + public RootGraphImplementor> createGraphForDynamicEntity(String entityName) { return delegate.createGraphForDynamicEntity( entityName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java index 230850cd2d4f..88b3eae4d299 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java @@ -23,7 +23,6 @@ import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EntityCopyObserverFactory; import org.hibernate.event.spi.EventEngine; -import org.hibernate.graph.RootGraph; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.metamodel.model.domain.JpaMetamodel; @@ -309,7 +308,7 @@ default RootGraphImplementor createEntityGraph(Class entityType) { } @Override - RootGraph> createGraphForDynamicEntity(String entityName); + RootGraphImplementor> createGraphForDynamicEntity(String entityName); /** * The best guess entity name for an entity not in an association diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractScrollableResults.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractScrollableResults.java index c270f37b3bb8..fcf2501bda98 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractScrollableResults.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractScrollableResults.java @@ -7,10 +7,10 @@ import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.query.spi.ScrollableResultsImplementor; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; import org.hibernate.sql.results.spi.RowReader; /** @@ -22,7 +22,7 @@ public abstract class AbstractScrollableResults implements ScrollableResultsImplementor { private final JdbcValues jdbcValues; private final JdbcValuesSourceProcessingOptions processingOptions; - private final JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState; + private final JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState; private final RowProcessingStateStandardImpl rowProcessingState; private final RowReader rowReader; private final SharedSessionContractImplementor persistenceContext; @@ -32,7 +32,7 @@ public abstract class AbstractScrollableResults implements ScrollableResultsI public AbstractScrollableResults( JdbcValues jdbcValues, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader, SharedSessionContractImplementor persistenceContext) { @@ -62,7 +62,7 @@ protected JdbcValuesSourceProcessingOptions getProcessingOptions() { return processingOptions; } - protected JdbcValuesSourceProcessingStateStandardImpl getJdbcValuesSourceProcessingState() { + protected JdbcValuesSourceProcessingState getJdbcValuesSourceProcessingState() { return jdbcValuesSourceProcessingState; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/FetchingScrollableResultsImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/FetchingScrollableResultsImpl.java index 91e7de1278b1..69d23a408d1e 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/FetchingScrollableResultsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/FetchingScrollableResultsImpl.java @@ -9,9 +9,9 @@ import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; import org.hibernate.sql.results.spi.LoadContexts; import org.hibernate.sql.results.spi.RowReader; @@ -31,7 +31,7 @@ public class FetchingScrollableResultsImpl extends AbstractScrollableResults< public FetchingScrollableResultsImpl( JdbcValues jdbcValues, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader, SharedSessionContractImplementor persistenceContext) { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/ScrollableResultsImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/ScrollableResultsImpl.java index 3d411aba70bf..1d46a49a2652 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/ScrollableResultsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/ScrollableResultsImpl.java @@ -7,9 +7,9 @@ import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; import org.hibernate.sql.results.spi.LoadContexts; import org.hibernate.sql.results.spi.RowReader; @@ -24,7 +24,7 @@ public class ScrollableResultsImpl extends AbstractScrollableResults { public ScrollableResultsImpl( JdbcValues jdbcValues, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader, SharedSessionContractImplementor persistenceContext) { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java index 911b5e40da0c..827d39224c5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -4,26 +4,15 @@ */ package org.hibernate.internal; -import java.io.IOException; -import java.io.InvalidObjectException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serial; -import java.sql.Connection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.naming.Reference; -import javax.naming.StringRefAddr; - +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.PersistenceUnitTransactionType; +import jakarta.persistence.PersistenceUnitUtil; +import jakarta.persistence.Query; +import jakarta.persistence.SynchronizationType; import jakarta.persistence.TypedQuery; +import jakarta.persistence.TypedQueryReference; import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.EntityNameResolver; import org.hibernate.FlushMode; @@ -67,12 +56,11 @@ import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; import org.hibernate.event.monitor.internal.EmptyEventMonitor; import org.hibernate.event.monitor.spi.EventMonitor; +import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EntityCopyObserverFactory; import org.hibernate.event.spi.EventEngine; -import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.generator.Generator; -import org.hibernate.graph.RootGraph; import org.hibernate.graph.internal.RootGraphImpl; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.integrator.spi.Integrator; @@ -113,14 +101,24 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.spi.TypeConfiguration; -import jakarta.persistence.EntityGraph; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceException; -import jakarta.persistence.PersistenceUnitTransactionType; -import jakarta.persistence.PersistenceUnitUtil; -import jakarta.persistence.Query; -import jakarta.persistence.SynchronizationType; -import jakarta.persistence.TypedQueryReference; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Function; import static jakarta.persistence.SynchronizationType.SYNCHRONIZED; import static java.util.Collections.emptySet; @@ -719,7 +717,7 @@ public boolean isOpen() { } @Override - public RootGraph> createGraphForDynamicEntity(String entityName) { + public RootGraphImplementor> createGraphForDynamicEntity(String entityName) { final var entity = getJpaMetamodel().entity( entityName ); if ( entity.getRepresentationMode() != RepresentationMode.MAP ) { throw new IllegalArgumentException( "Entity '" + entityName + "' is not a dynamic entity" ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java index bab3b5282121..a09b031ac868 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java @@ -43,7 +43,7 @@ import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBinding; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java index 6b90d2599b33..63a252fcaeaa 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java @@ -26,7 +26,7 @@ import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java index 40c417ff22d4..5c6afcd112d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java @@ -17,7 +17,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java index 8e7bb024964d..3e218ee7c8c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java @@ -25,7 +25,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java index d7526f847a2a..0bcbbc546313 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java @@ -22,7 +22,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java index 290dcb6912a7..9dc3cc36ac6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java @@ -28,7 +28,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.internal.ResultsHelper; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java index 2251b5eaf7bb..fc7ebf0436ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java @@ -33,7 +33,7 @@ import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java index c837e16735b7..3bb3500f30d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java @@ -23,7 +23,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import static org.hibernate.loader.ast.internal.LoaderHelper.loadByArrayParameter; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java index e98c8207a528..8b61f83e89d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java @@ -19,7 +19,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java index 753f40325d80..c9c702064122 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java @@ -20,7 +20,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadPlan.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadPlan.java index 7f4a185b99c9..7187dd90a64e 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadPlan.java @@ -6,7 +6,7 @@ import org.hibernate.loader.ast.spi.Loadable; import org.hibernate.metamodel.mapping.ModelPart; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcSelect; /** * Common contract for SQL AST based loading @@ -27,5 +27,5 @@ public interface LoadPlan { /** * The JdbcSelect for the load */ - JdbcOperationQuerySelect getJdbcSelect(); + JdbcSelect getJdbcSelect(); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java index 7444ff4e5f8f..fb56829d07e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java @@ -26,7 +26,7 @@ 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.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java index 9778970fbbd4..249536c18fe4 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java @@ -26,7 +26,7 @@ import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java index cbeadbac8271..fb93af668adf 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java @@ -10,7 +10,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java index 8b2915ec308f..f73fd9e21560 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java @@ -19,7 +19,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import static org.hibernate.loader.ast.internal.LoaderHelper.loadByArrayParameter; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java index 977733c496dd..75697020a27a 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java @@ -19,7 +19,7 @@ import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdLoadPlan.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdLoadPlan.java index 6e8206b5d00c..e9e3b99f59a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdLoadPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdLoadPlan.java @@ -4,8 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.List; - import org.hibernate.LockOptions; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -23,13 +21,15 @@ import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.sql.results.spi.RowTransformer; +import java.util.List; + /** * Describes a plan for loading an entity by identifier. * @@ -43,7 +43,7 @@ public class SingleIdLoadPlan implements SingleEntityLoadPlan { private final EntityMappingType entityMappingType; private final ModelPart restrictivePart; private final LockOptions lockOptions; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; private final JdbcParametersList jdbcParameters; public SingleIdLoadPlan( @@ -91,7 +91,7 @@ public ModelPart getRestrictivePart() { } @Override - public JdbcOperationQuerySelect getJdbcSelect() { + public JdbcSelect getJdbcSelect() { return jdbcSelect; } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java index 310a607c2dbe..0d769f8a811b 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java @@ -27,7 +27,7 @@ import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.Callback; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerSingularReturnImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java index a5b8be4ab175..e3650537cdbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java @@ -4,14 +4,7 @@ */ package org.hibernate.metamodel.mapping; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Supplier; - +import jakarta.persistence.Entity; import org.hibernate.Filter; import org.hibernate.Incubating; import org.hibernate.Internal; @@ -19,11 +12,13 @@ import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.loader.ast.spi.Loadable; import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; import org.hibernate.loader.ast.spi.NaturalIdLoader; import org.hibernate.mapping.Contributable; import org.hibernate.metamodel.UnsupportedMappingException; +import org.hibernate.metamodel.internal.EntityRepresentationStrategyMap; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityNameUse; @@ -42,7 +37,13 @@ import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.type.descriptor.java.JavaType; -import jakarta.persistence.Entity; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; @@ -84,16 +85,24 @@ default EntityRepresentationStrategy getRepresentationStrategy() { * and{@linkplain jakarta.persistence.InheritanceType#TABLE_PER_CLASS union} * inheritance hierarchies * - * @see #getIdentifierTableDetails() + * @see #getIdentifierTableDetails + * @see #forEachTableDetails */ TableDetails getMappedTableDetails(); /** * Details for the table that defines the identifier column(s) * for an entity hierarchy. + * + * @see #forEachTableDetails */ TableDetails getIdentifierTableDetails(); + /** + * Visit details for each table associated with the entity. + */ + void forEachTableDetails(Consumer consumer); + @Override default EntityMappingType findContainingEntityMapping() { return this; @@ -466,6 +475,15 @@ default String getImportedName() { return getEntityPersister().getImportedName(); } + default RootGraphImplementor createRootGraph(SharedSessionContractImplementor session) { + if ( getRepresentationStrategy() instanceof EntityRepresentationStrategyMap mapRep ) { + return session.getSessionFactory().createGraphForDynamicEntity( getEntityName() ); + } + else { + return session.getSessionFactory().createEntityGraph( getMappedJavaType().getJavaTypeClass() ); + } + } + interface ConstraintOrderedTableConsumer { void consume(String tableExpression, Supplier> tableKeyColumnVisitationSupplier); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java index 915ef35ee247..d7509fc159ec 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java @@ -4,6 +4,13 @@ */ package org.hibernate.metamodel.mapping; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; + import java.util.List; /** @@ -33,7 +40,7 @@ public interface TableDetails { /** * Details about the primary key of a table */ - interface KeyDetails { + interface KeyDetails extends SelectableMappings { /** * Number of columns */ @@ -49,10 +56,33 @@ interface KeyDetails { */ KeyColumn getKeyColumn(int position); + + @FunctionalInterface + interface KeyValueConsumer { + void consume(Object jdbcValue, KeyColumn columnMapping); + } + /** * Visit each key column */ void forEachKeyColumn(KeyColumnConsumer consumer); + + /** + * Break a key value down into its constituent parts, calling the consumer for each. + */ + void breakDownKeyJdbcValues( + Object domainValue, + KeyValueConsumer valueConsumer, + SharedSessionContractImplementor session); + + /** + * Create a DomainResult for selecting and retrieving the key. + */ + DomainResult createDomainResult( + NavigablePath navigablePath, + TableReference tableReference, + String resultVariable, + DomainResultCreationState creationState); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java index ddf3246de2aa..9b8473a974d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java @@ -25,7 +25,7 @@ import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index ba7571613fc9..783be55494c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -116,6 +116,7 @@ import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NaturalIdMapping; @@ -146,6 +147,7 @@ import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.models.internal.util.CollectionHelper; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.persister.entity.mutation.DeleteCoordinatorSoft; @@ -2677,6 +2679,11 @@ protected EntityTableMapping getTableMapping(int i) { return tableMappings[i]; } + @Override + public void forEachTableDetails(Consumer consumer) { + CollectionHelper.forEach( getTableMappings(), consumer ); + } + /** * Unfortunately we cannot directly use `SelectableMapping#getContainingTableExpression()` * as that blows up for attributes declared on super-type for union-subclass mappings @@ -3355,7 +3362,7 @@ protected EntityTableMapping[] buildTableMappings() { tableMappingBuilder = new TableMappingBuilder( tableExpression, relativePosition, - new EntityTableMapping.KeyMapping( keyColumns, identifierMapping ), + EntityTableMapping.createKeyMapping( keyColumns, identifierMapping ), !isIdentifierTable && isNullableTable( relativePosition ), inverseTable, isIdentifierTable, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index a6167ba68ff4..862ba7b05537 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -9,6 +9,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; import org.hibernate.AssertionFailure; @@ -1128,6 +1129,11 @@ public FilterAliasGenerator getFilterAliasGenerator(String rootAlias) { return new DynamicFilterAliasGenerator(subclassTableNameClosure, rootAlias); } + @Override + public void forEachTableDetails(Consumer consumer) { + super.forEachTableDetails( consumer ); + } + @Override public TableDetails getMappedTableDetails() { // Subtract the number of secondary tables (tableSpan - coreTableSpan) and get the last table mapping diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java index 25ba54d38ac6..68837d274df9 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java @@ -4,23 +4,32 @@ */ package org.hibernate.persister.entity.mutation; -import java.util.BitSet; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.jdbc.Expectation; +import org.hibernate.metamodel.mapping.BasicValuedModelPart; +import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectableMappings; import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.model.MutationType; import org.hibernate.sql.model.TableMapping; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.basic.BasicResult; + +import java.util.BitSet; +import java.util.List; +import java.util.Objects; /** * Descriptor for the mapping of a table relative to an entity @@ -224,30 +233,76 @@ public String toString() { return "TableMapping(" + tableName + ")"; } - @FunctionalInterface - public interface KeyValueConsumer { - void consume(Object jdbcValue, KeyColumn columnMapping); + public interface KeyMapping extends KeyDetails, SelectableMappings { } - public static class KeyMapping implements KeyDetails, SelectableMappings { - private final List keyColumns; - - private final ModelPart identifierPart; + public static KeyMapping createKeyMapping(List keyColumns, ModelPart identifierPart) { + if ( identifierPart instanceof EmbeddableValuedModelPart embeddedModelPart ) { + return new CompositeKeyMapping( keyColumns, embeddedModelPart ); + } + else { + assert keyColumns.size() == 1; + return new SimpleKeyMapping( keyColumns, (BasicValuedModelPart) identifierPart ); + } + } - public KeyMapping(List keyColumns, ModelPart identifierPart) { - assert keyColumns.size() == identifierPart.getJdbcTypeCount(); + public static abstract class AbstractKeyMapping implements KeyMapping { + protected final List keyColumns; + protected final ModelPart identifierPart; + public AbstractKeyMapping(List keyColumns, ModelPart identifierPart) { this.keyColumns = keyColumns; this.identifierPart = identifierPart; } + @Override + public List getKeyColumns() { + return keyColumns; + } + + @Override + public int getColumnCount() { + return getKeyColumns().size(); + } + + @Override + public KeyColumn getKeyColumn(int position) { + return getKeyColumns().get( position ); + } + + @Override + public void forEachKeyColumn(KeyColumnConsumer consumer) { + for ( int i = 0; i < getKeyColumns().size(); i++ ) { + consumer.consume( i, getKeyColumns().get( i ) ); + } + } + + @Override + public int getJdbcTypeCount() { + return getKeyColumns().size(); + } + + @Override + public SelectableMapping getSelectable(int columnIndex) { + return getKeyColumns().get( columnIndex ); + } + + @Override + public int forEachSelectable(int offset, SelectableConsumer consumer) { + for ( int i = 0; i < getKeyColumns().size(); i++ ) { + consumer.accept( i, getKeyColumns().get( i ) ); + } + + return getJdbcTypeCount(); + } + public void breakDownKeyJdbcValues( Object domainValue, KeyValueConsumer valueConsumer, SharedSessionContractImplementor session) { identifierPart.forEachJdbcValue( domainValue, - keyColumns, + getKeyColumns(), valueConsumer, (selectionIndex, keys, consumer, jdbcValue, jdbcMapping) -> consumer.consume( jdbcValue, @@ -257,49 +312,75 @@ public void breakDownKeyJdbcValues( ); } - @Override - public int getColumnCount() { - return keyColumns.size(); - } - - @Override - public List getKeyColumns() { - return keyColumns; + protected SqlSelection resolveSqlSelection( + TableReference tableReference, + KeyColumn keyColumn, + SqlAstCreationState creationState) { + final SqlExpressionResolver expressionResolver = creationState.getSqlExpressionResolver(); + return expressionResolver.resolveSqlSelection( + expressionResolver.resolveSqlExpression( tableReference, keyColumn ), + keyColumn.getJdbcMapping().getJdbcJavaType(), + null, + creationState.getCreationContext().getTypeConfiguration() + ); } @Override - public KeyColumn getKeyColumn(int position) { - return keyColumns.get( position ); + public int forEachSelectable(SelectableConsumer consumer) { + forEachKeyColumn( consumer::accept ); + return getJdbcTypeCount(); } + } - @Override - public void forEachKeyColumn(KeyColumnConsumer consumer) { - for ( int i = 0; i < keyColumns.size(); i++ ) { - consumer.consume( i, keyColumns.get( i ) ); - } - } + public static class SimpleKeyMapping extends AbstractKeyMapping { + private final KeyColumn keyColumn; - public void forEachKeyColumn(Consumer keyColumnConsumer) { - keyColumns.forEach( keyColumnConsumer ); + public SimpleKeyMapping(List keyColumns, BasicValuedModelPart identifierPart) { + super( keyColumns, identifierPart ); + this.keyColumn = keyColumns.get( 0 ); } @Override - public int getJdbcTypeCount() { - return keyColumns.size(); + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableReference tableReference, + String resultVariable, + DomainResultCreationState creationState) { + // create SqlSelection based on the underlying JdbcMapping + final SqlSelection sqlSelection = resolveSqlSelection( + tableReference, + keyColumn, + creationState.getSqlAstCreationState() + ); + + // return a BasicResult with conversion the entity class or entity-name + //noinspection unchecked,rawtypes + return new BasicResult( + sqlSelection.getValuesArrayPosition(), + resultVariable, + identifierPart.getJavaType(), + null, + navigablePath, + false, + !sqlSelection.isVirtual() + ); } + } - @Override - public SelectableMapping getSelectable(int columnIndex) { - return keyColumns.get( columnIndex ); + public static class CompositeKeyMapping extends AbstractKeyMapping { + public CompositeKeyMapping(List keyColumns, EmbeddableValuedModelPart identifierPart) { + super( keyColumns, identifierPart ); } @Override - public int forEachSelectable(int offset, SelectableConsumer consumer) { - for ( int i = 0; i < keyColumns.size(); i++ ) { - consumer.accept( i, keyColumns.get( i ) ); - } - - return getJdbcTypeCount(); + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableReference tableReference, + String resultVariable, + DomainResultCreationState creationState) { + // this will be challenging if the embeddable defines to-ones. + // just error for now. + throw new UnsupportedOperationException( "Not implemented yet" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/SqlOmittingQueryOptions.java b/hibernate-core/src/main/java/org/hibernate/query/spi/SqlOmittingQueryOptions.java index 60639eb75112..14aed357672e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/SqlOmittingQueryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/SqlOmittingQueryOptions.java @@ -6,7 +6,7 @@ import org.hibernate.Internal; import org.hibernate.LockOptions; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.spi.ListResultsConsumer; /** @@ -41,7 +41,7 @@ public static QueryOptions omitSqlQueryOptions(QueryOptions originalOptions) { return omitSqlQueryOptions( originalOptions, true, true ); } - public static QueryOptions omitSqlQueryOptions(QueryOptions originalOptions, JdbcOperationQuerySelect select) { + public static QueryOptions omitSqlQueryOptions(QueryOptions originalOptions, JdbcSelect select) { return omitSqlQueryOptions( originalOptions, !select.usesLimitParameters(), false ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeNonSelectQueryPlanImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeNonSelectQueryPlanImpl.java index 87baba840b00..5eb058a005fd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeNonSelectQueryPlanImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeNonSelectQueryPlanImpl.java @@ -15,7 +15,7 @@ import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutationNative; +import org.hibernate.sql.exec.internal.JdbcOperationQueryMutationNative; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java index 6b1c12e3c2a5..9ef013058a91 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeSelectQueryPlanImpl.java @@ -22,7 +22,7 @@ import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/CacheableSqmInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/CacheableSqmInterpretation.java index 25a94e195a00..331b4811fe21 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/CacheableSqmInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/CacheableSqmInterpretation.java @@ -8,7 +8,7 @@ import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.exec.spi.JdbcOperationQuery; +import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcParametersList; import java.util.List; @@ -17,7 +17,7 @@ /** * @since 7.1 */ -public record CacheableSqmInterpretation( +public record CacheableSqmInterpretation( S statement, J jdbcOperation, Map, Map, List>> jdbcParamsXref, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index d9b57879cd9e..468797dc4a5f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -4,12 +4,7 @@ */ package org.hibernate.query.sqm.internal; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import jakarta.persistence.Tuple; - import org.hibernate.AssertionFailure; import org.hibernate.InstantiationException; import org.hibernate.ScrollMode; @@ -28,6 +23,8 @@ import org.hibernate.query.spi.SelectQueryPlan; 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.sql.internal.SqmParameterInterpretation; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmDynamicInstantiation; @@ -39,10 +36,11 @@ import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; import org.hibernate.sql.results.internal.RowTransformerCheckingImpl; import org.hibernate.sql.results.internal.RowTransformerConstructorImpl; @@ -57,6 +55,10 @@ import org.hibernate.sql.results.spi.ResultsConsumer; import org.hibernate.sql.results.spi.RowTransformer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import static java.util.Collections.emptyList; import static org.hibernate.internal.util.ReflectHelper.isClass; import static org.hibernate.internal.util.collections.ArrayHelper.toStringArray; @@ -79,7 +81,7 @@ public class ConcreteSqmSelectQueryPlan implements SelectQueryPlan { private final SqmInterpreter, Void> listInterpreter; private final SqmInterpreter, ScrollMode> scrollInterpreter; - private volatile CacheableSqmInterpretation cacheableSqmInterpretation; + private volatile CacheableSqmInterpretation cacheableSqmInterpretation; public ConcreteSqmSelectQueryPlan( SqmSelectStatement sqm, @@ -97,7 +99,7 @@ public ConcreteSqmSelectQueryPlan( : ListResultsConsumer.UniqueSemantic.ALLOW; this.executeQueryInterpreter = (resultsConsumer, executionContext, sqmInterpretation, jdbcParameterBindings) -> { final SharedSessionContractImplementor session = executionContext.getSession(); - final JdbcOperationQuerySelect jdbcSelect = sqmInterpretation.jdbcOperation(); + final JdbcSelect jdbcSelect = sqmInterpretation.jdbcOperation(); try { final SubselectFetch.RegistrationHandler subSelectFetchKeyHandler = SubselectFetch.createRegistrationHandler( session.getPersistenceContext().getBatchFetchQueue(), @@ -127,7 +129,7 @@ public ConcreteSqmSelectQueryPlan( }; this.listInterpreter = (unused, executionContext, sqmInterpretation, jdbcParameterBindings) -> { final SharedSessionContractImplementor session = executionContext.getSession(); - final JdbcOperationQuerySelect jdbcSelect = sqmInterpretation.jdbcOperation(); + final JdbcSelect jdbcSelect = sqmInterpretation.jdbcOperation(); try { final SubselectFetch.RegistrationHandler subSelectFetchKeyHandler = SubselectFetch.createRegistrationHandler( session.getPersistenceContext().getBatchFetchQueue(), @@ -160,21 +162,11 @@ public ConcreteSqmSelectQueryPlan( this.scrollInterpreter = (scrollMode, executionContext, sqmInterpretation, jdbcParameterBindings) -> { final SharedSessionContractImplementor session = executionContext.getSession(); - final JdbcOperationQuerySelect jdbcSelect = sqmInterpretation.jdbcOperation(); + final JdbcSelect jdbcSelect = sqmInterpretation.jdbcOperation(); try { -// final SubselectFetch.RegistrationHandler subSelectFetchKeyHandler = SubselectFetch.createRegistrationHandler( -// executionContext.getSession().getPersistenceContext().getBatchFetchQueue(), -// sqmInterpretation.selectStatement, -// Collections.emptyList(), -// jdbcParameterBindings -// ); - - final JdbcSelectExecutor jdbcSelectExecutor = - session.getFactory().getJdbcServices().getJdbcSelectExecutor(); + final JdbcSelectExecutor jdbcSelectExecutor = session.getFactory().getJdbcServices().getJdbcSelectExecutor(); session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), true ); - final Expression fetchExpression = - sqmInterpretation.statement().getQueryPart() - .getFetchClauseExpression(); + final Expression fetchExpression = sqmInterpretation.statement().getQueryPart().getFetchClauseExpression(); final int resultCountEstimate = fetchExpression != null ? interpretIntExpression( fetchExpression, jdbcParameterBindings ) : -1; @@ -191,23 +183,12 @@ public ConcreteSqmSelectQueryPlan( domainParameterXref.clearExpansions(); } }; - - // todo (6.0) : we should do as much of the building as we can here - // since this is the thing cached, all the work we do here will - // be cached as well. - // NOTE : this statement ^^ is not affected by load-query-influencers, - // multi-valued parameter expansion, etc - because those all - // cause the plan to not be cached. - // NOTE2 (regarding NOTE) : not sure multi-valued parameter expansion, in - // particular, should veto caching of the plan. The expansion happens - // for each execution - see creation of `JdbcParameterBindings` in - // `#performList` and `#performScroll`. } protected static SqmJdbcExecutionContextAdapter listInterpreterExecutionContext( String hql, DomainQueryExecutionContext executionContext, - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, SubselectFetch.RegistrationHandler subSelectFetchKeyHandler) { return new MySqmJdbcExecutionContextAdapter( executionContext, jdbcSelect, subSelectFetchKeyHandler, hql ); } @@ -401,7 +382,7 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec // to protect access. However, synchronized is much simpler here. We will verify // during throughput testing whether this is an issue and consider changes then - CacheableSqmInterpretation localCopy = cacheableSqmInterpretation; + CacheableSqmInterpretation localCopy = cacheableSqmInterpretation; JdbcParameterBindings jdbcParameterBindings = null; executionContext.getSession().autoPreFlush(); @@ -456,7 +437,7 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec } // For Hibernate Reactive - protected JdbcParameterBindings createJdbcParameterBindings(CacheableSqmInterpretation sqmInterpretation, DomainQueryExecutionContext executionContext) { + protected JdbcParameterBindings createJdbcParameterBindings(CacheableSqmInterpretation sqmInterpretation, DomainQueryExecutionContext executionContext) { return SqmUtil.createJdbcParameterBindings( executionContext.getQueryParameterBindings(), domainParameterXref, @@ -473,7 +454,7 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter buildInterpretation( + protected static CacheableSqmInterpretation buildInterpretation( SqmSelectStatement sqm, DomainParameterXref domainParameterXref, DomainQueryExecutionContext executionContext, @@ -481,22 +462,23 @@ protected static CacheableSqmInterpretation sqmInterpretation = - sessionFactory.getQueryEngine().getSqmTranslatorFactory() - .createSelectTranslator( - sqm, - executionContext.getQueryOptions(), - domainParameterXref, - executionContext.getQueryParameterBindings(), - executionContext.getSession().getLoadQueryInfluencers(), - sessionFactory.getSqlTranslationEngine(), - true - ) - .translate(); - - final SqlAstTranslator selectTranslator = - sessionFactory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory() - .buildSelectTranslator( sessionFactory, sqmInterpretation.getSqlAst() ); + final SqmTranslatorFactory sqmTranslatorFactory = sessionFactory.getQueryEngine().getSqmTranslatorFactory(); + final SqmTranslator sqmTranslator = sqmTranslatorFactory.createSelectTranslator( + sqm, + executionContext.getQueryOptions(), + domainParameterXref, + executionContext.getQueryParameterBindings(), + executionContext.getSession().getLoadQueryInfluencers(), + sessionFactory.getSqlTranslationEngine(), + true + ); + final SqmTranslation sqmInterpretation = sqmTranslator.translate(); + + final SqlAstTranslator selectTranslator = sessionFactory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator( sessionFactory, sqmInterpretation.getSqlAst() ); final var jdbcParamsXref = generateJdbcParamsXref( domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam ); @@ -527,7 +509,7 @@ private interface SqmInterpreter { T interpret( X context, DomainQueryExecutionContext executionContext, - CacheableSqmInterpretation sqmInterpretation, + CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings); } @@ -537,7 +519,7 @@ private static class MySqmJdbcExecutionContextAdapter extends SqmJdbcExecutionCo public MySqmJdbcExecutionContextAdapter( DomainQueryExecutionContext executionContext, - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, SubselectFetch.RegistrationHandler subSelectFetchKeyHandler, String hql) { super( executionContext, jdbcSelect ); 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 8b83b14b48a8..377d1e7ac2b4 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 @@ -9,7 +9,7 @@ import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcSelect; import static org.hibernate.query.spi.SqlOmittingQueryOptions.omitSqlQueryOptions; @@ -44,7 +44,9 @@ private SqmJdbcExecutionContextAdapter(DomainQueryExecutionContext sqmExecutionC this.queryOptions = queryOptions; } - public SqmJdbcExecutionContextAdapter(DomainQueryExecutionContext sqmExecutionContext, JdbcOperationQuerySelect jdbcSelect) { + public SqmJdbcExecutionContextAdapter( + DomainQueryExecutionContext sqmExecutionContext, + JdbcSelect jdbcSelect) { this( sqmExecutionContext, omitSqlQueryOptions( sqmExecutionContext.getQueryOptions(), jdbcSelect ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MatchingIdSelectionHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MatchingIdSelectionHelper.java index c6d4e51e8d48..3999e5004ca1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MatchingIdSelectionHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MatchingIdSelectionHelper.java @@ -44,7 +44,7 @@ import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java index 7ea2a1916e14..d36d4c9b1892 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java @@ -54,7 +54,7 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java index cabbe520944d..ffe734e4e842 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java @@ -91,7 +91,7 @@ import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.Assignment; import org.hibernate.sql.ast.tree.update.UpdateStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/AbstractInlineHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/AbstractInlineHandler.java index ed7c80401ed5..f31e0028fb97 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/AbstractInlineHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/AbstractInlineHandler.java @@ -20,7 +20,7 @@ import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java index 726ff684ceba..77dd84d9e8f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java @@ -95,7 +95,7 @@ import org.hibernate.sql.exec.internal.JdbcParameterImpl; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEntityValuedModelPart.java index 431b82f37667..1cd2febb7912 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEntityValuedModelPart.java @@ -611,6 +611,11 @@ public TableDetails getIdentifierTableDetails() { return delegate.getEntityMappingType().getIdentifierTableDetails(); } + @Override + public void forEachTableDetails(Consumer consumer) { + delegate.getEntityMappingType().forEachTableDetails( consumer ); + } + @Override public void visitQuerySpaces(Consumer querySpaceConsumer) { delegate.getEntityMappingType().visitQuerySpaces( querySpaceConsumer ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslatorFactory.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslatorFactory.java index 91f35924bdc1..8727488b9187 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslatorFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslatorFactory.java @@ -8,7 +8,7 @@ import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.model.ast.TableMutation; import org.hibernate.sql.model.jdbc.JdbcMutationOperation; 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 60a8ae15fc37..da5ae8472264 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 @@ -9,6 +9,9 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import java.util.Collection; +import java.util.List; + /** * LockingClauseStrategy implementation for cases when a dialect * applies locking in the {@code FROM clause} (e.g., SQL Server). @@ -38,4 +41,14 @@ public boolean containsOuterJoins() { public void render(SqlAppender sqlAppender) { // nothing to do } + + @Override + public Collection getRootsToLock() { + return List.of(); + } + + @Override + public Collection getJoinsToLock() { + 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 bd09dc8f34ba..e73c6b35db02 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 @@ -4,6 +4,8 @@ */ package org.hibernate.sql.ast.internal; +import jakarta.persistence.Timeout; +import org.hibernate.AssertionFailure; import org.hibernate.LockOptions; import org.hibernate.Locking; import org.hibernate.dialect.Dialect; @@ -11,12 +13,10 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; -import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMappings; import org.hibernate.metamodel.mapping.TableDetails; -import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.mutation.EntityTableMapping; @@ -27,9 +27,11 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.exec.internal.lock.LockingTableGroup; 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; @@ -46,7 +48,7 @@ public class StandardLockingClauseStrategy implements LockingClauseStrategy { private final RowLockStrategy rowLockStrategy; private final PessimisticLockKind lockKind; private final Locking.Scope lockingScope; - private final int timeout; + private final Timeout timeout; /** * @implNote Tracked separately from {@linkplain #rootsToLock} and @@ -66,34 +68,37 @@ public StandardLockingClauseStrategy( 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 + assert lockKind != PessimisticLockKind.NONE; this.dialect = dialect; this.rowLockStrategy = rowLockStrategy; this.lockKind = lockKind; this.lockingScope = lockOptions.getScope(); - this.timeout = lockOptions.getTimeout().milliseconds(); + this.timeout = lockOptions.getTimeout(); } @Override public void registerRoot(TableGroup root) { - if ( !queryHasOuterJoins && !dialect.supportsOuterJoinForUpdate() ) { + if ( !queryHasOuterJoins ) { if ( CollectionHelper.isNotEmpty( root.getTableReferenceJoins() ) ) { // joined inheritance and/or secondary tables - inherently has outer joins queryHasOuterJoins = true; } } - if ( rowLockStrategy != RowLockStrategy.NONE ) { - if ( rootsToLock == null ) { - rootsToLock = new HashSet<>(); - } - rootsToLock.add( root ); + if ( rootsToLock == null ) { + rootsToLock = new HashSet<>(); } + rootsToLock.add( root ); } @Override public void registerJoin(TableGroupJoin join) { + checkForOuterJoins( 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 @@ -114,28 +119,36 @@ else if ( lockingScope == Locking.Scope.INCLUDE_FETCHES ) { } } - private void trackJoin(TableGroupJoin join) { - if ( !queryHasOuterJoins && !dialect.supportsOuterJoinForUpdate() ) { - final TableGroup joinedGroup = join.getJoinedGroup(); - if ( join.isInitialized() - && join.getJoinType() != SqlAstJoinType.INNER - && !joinedGroup.isVirtual() ) { - queryHasOuterJoins = true; - } - else if ( joinedGroup.getModelPart() instanceof EntityPersister entityMapping ) { - if ( entityMapping.hasMultipleTables() ) { - // joined inheritance and/or secondary tables - inherently has outer joins - queryHasOuterJoins = true; + private void checkForOuterJoins(TableGroupJoin join) { + if ( queryHasOuterJoins ) { + // perf out + return; + } + queryHasOuterJoins = hasOuterJoin( join ); + } + + private boolean hasOuterJoin(TableGroupJoin join) { + final TableGroup joinedGroup = join.getJoinedGroup(); + if ( join.isInitialized() + && join.getJoinType() != SqlAstJoinType.INNER + && !joinedGroup.isVirtual() ) { + return true; + } + if ( !CollectionHelper.isEmpty( joinedGroup.getTableReferenceJoins() ) ) { + for ( TableReferenceJoin tableReferenceJoin : joinedGroup.getTableReferenceJoins() ) { + if ( tableReferenceJoin.getJoinType() != SqlAstJoinType.INNER ) { + return true; } } } + return false; + } - if ( rowLockStrategy != RowLockStrategy.NONE ) { - if ( joinsToLock == null ) { - joinsToLock = new LinkedHashSet<>(); - } - joinsToLock.add( join ); + private void trackJoin(TableGroupJoin join) { + if ( joinsToLock == null ) { + joinsToLock = new LinkedHashSet<>(); } + joinsToLock.add( join ); } @Override @@ -149,6 +162,16 @@ 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 ) { @@ -166,6 +189,10 @@ protected void renderLockFragment(SqlAppender sqlAppender) { } private String collectLockItems() { + if ( rowLockStrategy == null ) { + return ""; + } + final List lockItems = new ArrayList<>(); for ( TableGroup root : rootsToLock ) { collectLockItems( root, lockItems ); @@ -217,13 +244,9 @@ private void addTableAliases(TableGroup tableGroup, List lockItems) { } private void addColumnRefs(TableGroup tableGroup, List lockItems) { - final String[] keyColumns = determineKeyColumnNames( tableGroup.getModelPart() ); + final String[] keyColumns = determineKeyColumnNames( tableGroup ); final String tableAlias = tableGroup.getPrimaryTableReference().getIdentificationVariable(); for ( int i = 0; i < keyColumns.length; i++ ) { - // NOTE: in some tests with Oracle, the qualifiers are being applied twice; - // still need to track that down. possibly, unexpected calls to - // `Dialect#applyLocksToSql`? - assert !keyColumns[i].contains( "." ); lockItems.add( tableAlias + "." + keyColumns[i] ); } @@ -274,30 +297,33 @@ else if ( modelPart instanceof EntityAssociationMapping entityAssociationMapping } } - private String[] determineKeyColumnNames(ModelPart modelPart) { - if ( modelPart instanceof EntityPersister entityPersister ) { + private String[] determineKeyColumnNames(TableGroup tableGroup) { + if ( tableGroup instanceof LockingTableGroup lockingTableGroup ) { + return extractColumnNames( lockingTableGroup.getKeyColumnMappings() ); + } + else if ( tableGroup.getModelPart() instanceof EntityPersister entityPersister ) { return entityPersister.getIdentifierColumnNames(); } - else if ( modelPart instanceof PluralAttributeMapping pluralAttributeMapping ) { - final ForeignKeyDescriptor keyDescriptor = pluralAttributeMapping.getKeyDescriptor(); - final ValuedModelPart keyPart = keyDescriptor.getKeyPart(); - if ( keyPart.getJdbcTypeCount() == 1 ) { - return new String[] { keyPart.getSelectable( 0 ).getSelectableName() }; - } - - final ArrayList results = CollectionHelper.arrayList( keyPart.getJdbcTypeCount() ); - keyPart.forEachSelectable( (index, selectable) -> { - if ( !selectable.isFormula() ) { - results.add( selectable.getSelectableName() ); - } - } ); - return results.toArray( new String[0] ); + else if ( tableGroup.getModelPart() instanceof PluralAttributeMapping pluralAttributeMapping ) { + return extractColumnNames( pluralAttributeMapping.getKeyDescriptor() ); } - else if ( modelPart instanceof EntityAssociationMapping entityAssociationMapping ) { - return determineKeyColumnNames( entityAssociationMapping.getAssociatedEntityMappingType() ); + else if ( tableGroup.getModelPart() instanceof EntityAssociationMapping entityAssociationMapping ) { + return extractColumnNames( entityAssociationMapping.getAssociatedEntityMappingType().getIdentifierMapping() ); } else { - return null; + throw new AssertionFailure( "Unable to determine columns for locking" ); } } + + private static String[] extractColumnNames(SelectableMappings keyColumnMappings) { + if ( keyColumnMappings.getJdbcTypeCount() == 1 ) { + return new String[] { keyColumnMappings.getSelectable( 0 ).getSelectableName() }; + } + + final String[] results = new String[ keyColumnMappings.getJdbcTypeCount() ]; + keyColumnMappings.forEachSelectable( (selectionIndex, selectableMapping) -> { + results[selectionIndex] = selectableMapping.getSelectableName(); + } ); + return results; + } } 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 440526445d30..45ca7de8a921 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 @@ -14,6 +14,8 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.SelectItemReferenceStrategy; +import org.hibernate.dialect.lock.spi.LockTimeoutType; +import org.hibernate.dialect.lock.spi.LockingSupport; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -70,8 +72,8 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlTreeCreationException; -import org.hibernate.sql.ast.internal.TableGroupHelper; import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; +import org.hibernate.sql.ast.internal.TableGroupHelper; import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.SqlAstNode; @@ -86,7 +88,44 @@ import org.hibernate.sql.ast.tree.cte.SearchClauseSpecification; import org.hibernate.sql.ast.tree.cte.SelfRenderingCteObject; import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.*; +import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression; +import org.hibernate.sql.ast.tree.expression.Any; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; +import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Collation; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Duration; +import org.hibernate.sql.ast.tree.expression.DurationUnit; +import org.hibernate.sql.ast.tree.expression.EmbeddableTypeLiteral; +import org.hibernate.sql.ast.tree.expression.EntityTypeLiteral; +import org.hibernate.sql.ast.tree.expression.Every; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.ExtractUnit; +import org.hibernate.sql.ast.tree.expression.Format; +import org.hibernate.sql.ast.tree.expression.FunctionExpression; +import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.LiteralAsParameter; +import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; +import org.hibernate.sql.ast.tree.expression.NestedColumnReference; +import org.hibernate.sql.ast.tree.expression.OrderedSetAggregateFunctionExpression; +import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; +import org.hibernate.sql.ast.tree.expression.Star; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.expression.TrimSpecification; +import org.hibernate.sql.ast.tree.expression.UnaryOperation; +import org.hibernate.sql.ast.tree.expression.UnparsedNumericLiteral; import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.from.FunctionTableReference; @@ -133,19 +172,24 @@ import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.ExecutionException; import org.hibernate.sql.exec.internal.AbstractJdbcParameter; +import org.hibernate.sql.exec.internal.JdbcOperationQueryDelete; import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQueryUpdate; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcSelectWithActions; +import org.hibernate.sql.exec.internal.LockTimeoutHandler; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; +import org.hibernate.sql.exec.internal.lock.CollectionLockingAction; +import org.hibernate.sql.exec.internal.lock.FollowOnLockingAction; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcLockStrategy; import org.hibernate.sql.exec.spi.JdbcOperation; -import org.hibernate.sql.exec.spi.JdbcOperationQueryDelete; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; -import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBinding; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.ast.ColumnValueParameter; import org.hibernate.sql.model.ast.ColumnWriteFragment; @@ -275,6 +319,8 @@ public abstract class AbstractSqlAstTranslator implemen * be applied. Generally this will be the root QuerySpec, but, well, Oracle... */ private QuerySpec lockingTarget; + private LockingClauseStrategy lockingClauseStrategy; + private LockOptions lockOptions; private final Dialect dialect; private final Set affectedTableNames = new HashSet<>(); @@ -302,7 +348,6 @@ public abstract class AbstractSqlAstTranslator implemen private transient BasicType stringType; private transient BasicType booleanType; - private LockOptions lockOptions; private Limit limit; private JdbcParameter offsetParameter; private JdbcParameter limitParameter; @@ -819,14 +864,17 @@ protected String getUniqueConstraintNameThatMayFail(InsertSelectStatement sqlAst } } - protected JdbcOperationQuerySelect translateSelect(SelectStatement selectStatement) { + protected JdbcSelect translateSelect(SelectStatement selectStatement) { logDomainResultGraph( selectStatement.getDomainResultDescriptors() ); logSqlAst( selectStatement ); + // we need to make a cope here for later since visitSelectStatement clears it :( + final LockOptions lockOptions = this.lockOptions; + visitSelectStatement( selectStatement ); final int rowsToSkip; - return new JdbcOperationQuerySelect( + final JdbcOperationQuerySelect jdbcSelect = new JdbcOperationQuerySelect( getSql(), getParameterBinders(), buildJdbcValuesMappingProducer( selectStatement ), @@ -838,6 +886,33 @@ protected JdbcOperationQuerySelect translateSelect(SelectStatement selectStateme getOffsetParameter(), getLimitParameter() ); + + if ( lockOptions == null || !lockOptions.getLockMode().isPessimistic() ) { + return jdbcSelect; + } + + final LockingSupport lockingSupport = getDialect().getLockingSupport(); + final LockingSupport.Metadata lockingSupportMetadata = lockingSupport.getMetadata(); + + final JdbcSelectWithActions.Builder builder = new JdbcSelectWithActions.Builder( jdbcSelect ); + + final LockTimeoutType lockTimeoutType = lockingSupportMetadata.getLockTimeoutType( lockOptions.getTimeout() ); + if ( lockTimeoutType == LockTimeoutType.CONNECTION ) { + builder.addSecondaryActionPair( new LockTimeoutHandler( + lockOptions.getTimeout(), + lockingSupport.getConnectionLockTimeoutStrategy() + ) ); + } + + final LockStrategy lockStrategy = determineLockingStrategy( lockingTarget, lockOptions.getFollowOnStrategy() ); + if ( lockStrategy == LockStrategy.FOLLOW_ON ) { + FollowOnLockingAction.apply( lockOptions, lockingTarget, lockingClauseStrategy, builder ); + } + else if ( lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS ) { + CollectionLockingAction.apply( lockOptions, lockingTarget, builder ); + } + + return builder.build(); } private JdbcValuesMappingProducer buildJdbcValuesMappingProducer(SelectStatement selectStatement) { @@ -1677,8 +1752,6 @@ protected void visitValuesListEmulateSelectUnion(List valuesList) { } } - private LockingClauseStrategy lockingClauseStrategy; - protected LockingClauseStrategy getLockingClauseStrategy() { return lockingClauseStrategy; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/LockingClauseStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/LockingClauseStrategy.java index 883940221707..fe55f09dca5d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/LockingClauseStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/LockingClauseStrategy.java @@ -7,6 +7,8 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import java.util.Collection; + /** * Strategy for dealing with locking via a SQL {@code FOR UPDATE (OF)} * clause. @@ -40,4 +42,7 @@ public interface LockingClauseStrategy { boolean containsOuterJoins(); void render(SqlAppender sqlAppender); + + Collection getRootsToLock(); + Collection getJoinsToLock(); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslator.java index 21b792d8a680..4c9a13c4c476 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslator.java @@ -7,7 +7,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; /** * The final phase of query translation. Here we take the SQL AST an diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslatorFactory.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslatorFactory.java index 8bedd80c6213..59cf7cc5c62d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslatorFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/StandardSqlAstTranslatorFactory.java @@ -12,7 +12,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.model.ast.TableMutation; import org.hibernate.sql.model.jdbc.JdbcMutationOperation; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/Statement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/Statement.java index 0dac707a08f2..523431629b93 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/Statement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/Statement.java @@ -16,4 +16,11 @@ public interface Statement { * Visitation */ void accept(SqlAstWalker walker); + + /** + * Whether this statement is a selection and will return results. + */ + default boolean isSelection() { + return false; + } } 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 adc9295ae673..a15043918750 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 @@ -46,6 +46,11 @@ public SelectStatement( this.domainResults = domainResults; } + @Override + public boolean isSelection() { + return true; + } + public QuerySpec getQuerySpec() { return queryPart.getFirstQuerySpec(); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/AbstractJdbcOperationQuery.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQuery.java similarity index 90% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/spi/AbstractJdbcOperationQuery.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQuery.java index 56803a6f63db..50541b86e9bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/AbstractJdbcOperationQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQuery.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.sql.exec.spi; +package org.hibernate.sql.exec.internal; import java.util.List; import java.util.Map; @@ -10,6 +10,10 @@ import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.spi.JdbcOperationQuery; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBinding; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; import static java.util.Collections.emptyMap; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java index 5e24a4447566..4f1aaf8c77eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Set; -import org.hibernate.sql.exec.spi.AbstractJdbcOperationQuery; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; import org.hibernate.sql.exec.spi.JdbcParameterBinder; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryDelete.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryDelete.java similarity index 75% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryDelete.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryDelete.java index e088b9c905ae..b0d5a4b371fd 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryDelete.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryDelete.java @@ -2,13 +2,16 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.sql.exec.spi; +package org.hibernate.sql.exec.internal; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBinding; /** * @author Steve Ebersole diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutationNative.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryMutationNative.java similarity index 84% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutationNative.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryMutationNative.java index 7d62b065ee18..c59ccf2ff4c3 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutationNative.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryMutationNative.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.sql.exec.spi; +package org.hibernate.sql.exec.internal; import java.util.Collections; import java.util.List; @@ -11,6 +11,10 @@ import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBinding; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; /** * Executable JDBC command diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuerySelect.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQuerySelect.java similarity index 83% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuerySelect.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQuerySelect.java index c9a2e63bad68..61893f9bd8c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuerySelect.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQuerySelect.java @@ -2,25 +2,37 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.sql.exec.spi; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; +package org.hibernate.sql.exec.internal; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcLockStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBinding; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.StatementAccess; +import org.hibernate.sql.exec.spi.JdbcSelect; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.type.descriptor.java.JavaType; +import java.sql.Connection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + /** - * Executable JDBC command + * Executable JDBC command produced from some form of Query. * * @author Steve Ebersole */ -public class JdbcOperationQuerySelect extends AbstractJdbcOperationQuery { +public class JdbcOperationQuerySelect + extends AbstractJdbcOperationQuery + implements JdbcSelect { private final JdbcValuesMappingProducer jdbcValuesMappingProducer; private final int rowsToSkip; private final int maxRows; @@ -67,30 +79,49 @@ public JdbcOperationQuerySelect( this.limitParameter = limitParameter; } + @Override public JdbcValuesMappingProducer getJdbcValuesMappingProducer() { return jdbcValuesMappingProducer; } + @Override public int getRowsToSkip() { return rowsToSkip; } + @Override public int getMaxRows() { return maxRows; } - public boolean usesLimitParameters() { - return offsetParameter != null || limitParameter != null; + @Override + public @Nullable LoadedValuesCollector getLoadedValuesCollector() { + return null; } - public JdbcParameter getOffsetParameter() { - return offsetParameter; + @Override + public void performPreActions(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + } + + @Override + public void performPostAction(boolean succeeded, StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + } + + @Override + public boolean usesLimitParameters() { + return offsetParameter != null || limitParameter != null; } + @Override public JdbcParameter getLimitParameter() { return limitParameter; } + public JdbcParameter getOffsetParameter() { + return offsetParameter; + } + + @Override public JdbcLockStrategy getLockStrategy() { return jdbcLockStrategy; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryUpdate.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryUpdate.java similarity index 75% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryUpdate.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryUpdate.java index 87247a3b56cf..aa4cf25fe7eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryUpdate.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryUpdate.java @@ -2,13 +2,16 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.sql.exec.spi; +package org.hibernate.sql.exec.internal; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBinding; /** * @author Steve Ebersole diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcParameterBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcParameterBindingsImpl.java index bc815b2b78d9..ef1bad2b161d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcParameterBindingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcParameterBindingsImpl.java @@ -165,4 +165,11 @@ public void clear() { bindingMap.clear(); } } + + /** + * For testing. + */ + public Map getBindingMap() { + return bindingMap; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java index 97e2b01c2a93..eb4c7c3a80ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java @@ -4,6 +4,7 @@ */ package org.hibernate.sql.exec.internal; +import java.sql.Connection; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -14,10 +15,11 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.query.TupleTransformer; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.ResultsHelper; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; @@ -61,7 +63,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor { @Override public T executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -82,7 +84,7 @@ public T executeQuery( @Override public T executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -118,7 +120,7 @@ public T executeQuery( } private T doExecuteQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -187,8 +189,11 @@ public boolean shouldReturnProxies() { } }; - final var valuesProcessingState = - new JdbcValuesSourceProcessingStateStandardImpl( executionContext, processingOptions ); + final var valuesProcessingState = new JdbcValuesSourceProcessingStateStandardImpl( + jdbcSelect.getLoadedValuesCollector(), + processingOptions, + executionContext + ); final RowReader rowReader = ResultsHelper.createRowReader( session.getFactory(), @@ -197,27 +202,45 @@ public boolean shouldReturnProxies() { jdbcValues ); - final var rowProcessingState = - new RowProcessingStateStandardImpl( valuesProcessingState, executionContext, rowReader, jdbcValues ); + final var rowProcessingState = new RowProcessingStateStandardImpl( valuesProcessingState, executionContext, rowReader, jdbcValues ); - final T result = resultsConsumer.consume( - jdbcValues, - session, - processingOptions, - valuesProcessingState, - rowProcessingState, - rowReader + final LogicalConnectionImplementor logicalConnection = session.getJdbcCoordinator().getLogicalConnection(); + final SessionFactoryImplementor sessionFactory = session.getSessionFactory(); + + final Connection connection = logicalConnection.getPhysicalConnection(); + final StatementAccessImpl statementAccess = new StatementAccessImpl( + connection, + logicalConnection, + sessionFactory ); + jdbcSelect.performPreActions( statementAccess, connection, executionContext ); - if ( stats ) { - logQueryStatistics( jdbcSelect, executionContext, startTime, result, statistics ); - } + try { + final T result = resultsConsumer.consume( + jdbcValues, + session, + processingOptions, + valuesProcessingState, + rowProcessingState, + rowReader + ); + + jdbcSelect.performPostAction( true, statementAccess, connection, executionContext ); - return result; + if ( stats ) { + logQueryStatistics( jdbcSelect, executionContext, startTime, result, statistics ); + } + + return result; + } + catch (RuntimeException e) { + jdbcSelect.performPostAction( false, statementAccess, connection, executionContext ); + throw e; + } } private void logQueryStatistics( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, ExecutionContext executionContext, long startTime, Object result, @@ -231,11 +254,9 @@ private void logQueryStatistics( statistics.queryExecuted( query, rows, milliseconds ); } - private static RowTransformer getRowTransformer(ExecutionContext executionContext, JdbcValues jdbcValues) { + protected static RowTransformer getRowTransformer(ExecutionContext executionContext, JdbcValues jdbcValues) { @SuppressWarnings("unchecked") - final var tupleTransformer = - (TupleTransformer) - executionContext.getQueryOptions().getTupleTransformer(); + final var tupleTransformer = (TupleTransformer) executionContext.getQueryOptions().getTupleTransformer(); if ( tupleTransformer == null ) { return RowTransformerStandardImpl.instance(); } @@ -249,16 +270,16 @@ private static RowTransformer getRowTransformer(ExecutionContext executio } } - private int getResultSize(T result) { + protected int getResultSize(T result) { return result instanceof List list ? list.size() : -1; } - private JdbcValues resolveJdbcValuesSource( + protected JdbcValues resolveJdbcValuesSource( String queryIdentifier, - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, boolean canBeCached, ExecutionContext executionContext, - DeferredResultSetAccess resultSetAccess) { + ResultSetAccess resultSetAccess) { final var session = executionContext.getSession(); final var factory = session.getFactory(); final boolean queryCacheEnabled = factory.getSessionFactoryOptions().isQueryCacheEnabled(); @@ -283,9 +304,8 @@ private JdbcValues resolveJdbcValuesSource( SQL_EXEC_LOGGER.tracef( "Affected query spaces %s", querySpaces ); } - final var queryCache = - factory.getCache() - .getQueryResultsCache( queryOptions.getResultCacheRegionName() ); + final var queryCache = factory.getCache() + .getQueryResultsCache( queryOptions.getResultCacheRegionName() ); queryResultsCacheKey = QueryKey.from( jdbcSelect.getSqlString(), @@ -354,7 +374,7 @@ private JdbcValues resolveJdbcValuesSource( private static AbstractJdbcValues resolveJdbcValues( String queryIdentifier, ExecutionContext executionContext, - DeferredResultSetAccess resultSetAccess, + ResultSetAccess resultSetAccess, List cachedResults, QueryKey queryResultsCacheKey, JdbcValuesMappingProducer mappingProducer, @@ -379,7 +399,7 @@ private static AbstractJdbcValues resolveJdbcValues( queryResultsCacheKey, queryIdentifier, executionContext.getQueryOptions(), - resultSetAccess.usesFollowOnLocking(), + false, jdbcValuesMapping, metadataForCache, executionContext 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 new file mode 100644 index 000000000000..fb8f856ef1bb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectWithActions.java @@ -0,0 +1,278 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcLockStrategy; +import org.hibernate.sql.exec.spi.JdbcOperationQuery; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBinding; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; +import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.PreAction; +import org.hibernate.sql.exec.spi.SecondaryAction; +import org.hibernate.sql.exec.spi.StatementAccess; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Steve Ebersole + */ +public class JdbcSelectWithActions implements JdbcOperationQuery, JdbcSelect { + private final JdbcOperationQuerySelect primaryOperation; + + private final LoadedValuesCollector loadedValuesCollector; + private final PreAction[] preActions; + private final PostAction[] postActions; + + public JdbcSelectWithActions( + JdbcOperationQuerySelect primaryOperation, + LoadedValuesCollector loadedValuesCollector, + PreAction[] preActions, + PostAction[] postActions) { + this.primaryOperation = primaryOperation; + this.loadedValuesCollector = loadedValuesCollector; + this.preActions = preActions; + this.postActions = postActions; + } + + public JdbcSelectWithActions( + JdbcOperationQuerySelect primaryAction, + LoadedValuesCollector loadedValuesCollector) { + this( primaryAction, loadedValuesCollector, null, null ); + } + + @Override + public JdbcValuesMappingProducer getJdbcValuesMappingProducer() { + return primaryOperation.getJdbcValuesMappingProducer(); + } + + @Override + public JdbcLockStrategy getLockStrategy() { + return primaryOperation.getLockStrategy(); + } + + @Override + public boolean usesLimitParameters() { + return primaryOperation.usesLimitParameters(); + } + + @Override + public JdbcParameter getLimitParameter() { + return primaryOperation.getLimitParameter(); + } + + @Override + public int getRowsToSkip() { + return primaryOperation.getRowsToSkip(); + } + + @Override + public int getMaxRows() { + return primaryOperation.getMaxRows(); + } + + @Override + public @Nullable LoadedValuesCollector getLoadedValuesCollector() { + return loadedValuesCollector; + } + + @Override + public void performPreActions(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + if ( preActions == null ) { + return; + } + + for ( int i = 0; i < preActions.length; i++ ) { + preActions[i].performPreAction( jdbcStatementAccess, jdbcConnection, executionContext ); + } + } + + @Override + public void performPostAction(boolean succeeded, StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + if ( postActions != null ) { + for ( int i = 0; i < postActions.length; i++ ) { + if ( succeeded || postActions[i].shouldRunAfterFail() ) { + postActions[i].performPostAction( jdbcStatementAccess, jdbcConnection, executionContext ); + } + } + } + if ( loadedValuesCollector != null ) { + loadedValuesCollector.clear(); + } + } + + @Override + public Set getAffectedTableNames() { + // NOTE: the complete set of affected table-names might be + // slightly expanded here accounting for pre- and post-actions + return primaryOperation.getAffectedTableNames(); + } + + @Override + public String getSqlString() { + return primaryOperation.getSqlString(); + } + + @Override + public List getParameterBinders() { + return primaryOperation.getParameterBinders(); + } + + @Override + public boolean dependsOnParameterBindings() { + return primaryOperation.dependsOnParameterBindings(); + } + + @Override + public Map getAppliedParameters() { + return primaryOperation.getAppliedParameters(); + } + + @Override + public boolean isCompatibleWith(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { + // todo : is this enough here? + return primaryOperation.isCompatibleWith( jdbcParameterBindings, queryOptions ); + } + + public static class Builder { + private final JdbcOperationQuerySelect primaryAction; + + private LoadedValuesCollector loadedValuesCollector; + protected List preActions; + protected List postActions; + + public Builder(JdbcOperationQuerySelect primaryAction) { + this.primaryAction = primaryAction; + } + + @SuppressWarnings("UnusedReturnValue") + public Builder setLoadedValuesCollector(LoadedValuesCollector loadedValuesCollector) { + this.loadedValuesCollector = loadedValuesCollector; + return this; + } + + public JdbcSelect build() { + if ( preActions == null && postActions == null ) { + assert loadedValuesCollector == null; + return primaryAction; + } + final PreAction[] preActions = toPreActionArray( this.preActions ); + final PostAction[] postActions = toPostActionArray( this.postActions ); + return new JdbcSelectWithActions( primaryAction, loadedValuesCollector, preActions, postActions ); + } + + /** + * Appends the {@code actions} to the growing list of pre-actions, + * executed (in order) after all currently registered actions. + * + * @return {@code this}, for method chaining. + */ + public Builder appendPreAction(PreAction... actions) { + if ( preActions == null ) { + preActions = new ArrayList<>(); + } + Collections.addAll( preActions, actions ); + return this; + } + + /** + * Prepends the {@code actions} to the growing list of pre-actions + * + * @return {@code this}, for method chaining. + */ + public Builder prependPreAction(PreAction... actions) { + if ( preActions == null ) { + preActions = new ArrayList<>(); + } + // todo (DatabaseOperation) : should we invert the order of the incoming actions? + Collections.addAll( preActions, actions ); + return this; + } + + /** + * Appends the {@code actions} to the growing list of post-actions + * + * @return {@code this}, for method chaining. + */ + public Builder appendPostAction(PostAction... actions) { + if ( postActions == null ) { + postActions = new ArrayList<>(); + } + Collections.addAll( postActions, actions ); + return this; + } + + /** + * Prepends the {@code actions} to the growing list of post-actions + * + * @return {@code this}, for method chaining. + */ + public Builder prependPostAction(PostAction... actions) { + if ( postActions == null ) { + postActions = new ArrayList<>(); + } + // todo (DatabaseOperation) : should we invert the order of the incoming actions? + Collections.addAll( postActions, actions ); + return this; + } + + /** + * Adds a secondary action pair. + * Assumes the {@code action} implements both {@linkplain PreAction} and {@linkplain PostAction}. + * + * @apiNote Prefer {@linkplain #addSecondaryActionPair(PreAction, PostAction)} to avoid + * the casts needed here. + * + * @see #prependPreAction + * @see #appendPostAction + * + * @return {@code this}, for method chaining. + */ + public Builder addSecondaryActionPair(SecondaryAction action) { + return addSecondaryActionPair( (PreAction) action, (PostAction) action ); + } + + /** + * Adds a PreAction/PostAction pair. + * + * @see #prependPreAction + * @see #appendPostAction + * + * @return {@code this}, for method chaining. + */ + public Builder addSecondaryActionPair(PreAction preAction, PostAction postAction) { + prependPreAction( preAction ); + appendPostAction( postAction ); + return this; + } + + private static PreAction[] toPreActionArray(List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + return actions.toArray( new PreAction[0] ); + } + + private static PostAction[] toPostActionArray(List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + return actions.toArray( new PostAction[0] ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/LockTimeoutHandler.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/LockTimeoutHandler.java new file mode 100644 index 000000000000..f3e38b738a14 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/LockTimeoutHandler.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import jakarta.persistence.Timeout; +import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.PreAction; +import org.hibernate.sql.exec.spi.StatementAccess; + +import java.sql.Connection; + +/** + * Handles lock timeouts using setting on the JDBC Connection. + * + * @see ConnectionLockTimeoutStrategy + * + * @author Steve Ebersole + */ +public class LockTimeoutHandler implements PreAction, PostAction { + private final ConnectionLockTimeoutStrategy lockTimeoutStrategy; + private final Timeout timeout; + + private Timeout baseline; + private boolean setTimeout; + + public LockTimeoutHandler(Timeout timeout, ConnectionLockTimeoutStrategy lockTimeoutStrategy) { + this.timeout = timeout; + this.lockTimeoutStrategy = lockTimeoutStrategy; + } + + public Timeout getBaseline() { + return baseline; + } + + @Override + public void performPreAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + // first, get the baseline (for post-action) + baseline = lockTimeoutStrategy.getLockTimeout( jdbcConnection, factory ); + + // now set the timeout + lockTimeoutStrategy.setLockTimeout( timeout, jdbcConnection, factory ); + setTimeout = true; + } + + @Override + public void performPostAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + // reset the timeout + lockTimeoutStrategy.setLockTimeout( baseline, jdbcConnection, factory ); + } + + @Override + public boolean shouldRunAfterFail() { + // if we set the timeout in the pre-action, we should always reset it in post-action + return setTimeout; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java new file mode 100644 index 000000000000..db8b85e9fa84 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.resource.jdbc.LogicalConnection; +import org.hibernate.sql.exec.spi.StatementAccess; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Lazy access to a JDBC {@linkplain Statement}. + * Manages various tasks around creation and ensuring it gets cleaned up. + * + * @author Steve Ebersole + */ +public class StatementAccessImpl implements StatementAccess { + private final Connection jdbcConnection; + private final LogicalConnection logicalConnection; + private final SessionFactoryImplementor factory; + + private Statement jdbcStatement; + + public StatementAccessImpl(Connection jdbcConnection, LogicalConnection logicalConnection, SessionFactoryImplementor factory) { + this.jdbcConnection = jdbcConnection; + this.logicalConnection = logicalConnection; + this.factory = factory; + } + + @Override public Statement getJdbcStatement() { + if ( jdbcStatement == null ) { + try { + jdbcStatement = jdbcConnection.createStatement(); + logicalConnection.getResourceRegistry().register( jdbcStatement, false ); + } + catch (SQLException e) { + throw factory.getJdbcServices() + .getSqlExceptionHelper() + .convert( e, "Unable to create JDBC Statement" ); + } + } + return jdbcStatement; + } + + public void release() { + if ( jdbcStatement != null ) { + try { + jdbcStatement.close(); + logicalConnection.getResourceRegistry().release( jdbcStatement ); + } + catch (SQLException e) { + throw factory.getJdbcServices() + .getSqlExceptionHelper() + .convert( e, "Unable to release JDBC Statement" ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementCreatorHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementCreatorHelper.java similarity index 93% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementCreatorHelper.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementCreatorHelper.java index dcfb03facc33..48fd725196ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementCreatorHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementCreatorHelper.java @@ -2,7 +2,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.sql.exec.spi; +package org.hibernate.sql.exec.internal; import java.sql.PreparedStatement; 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 new file mode 100644 index 000000000000..f9f69385aded --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionLockingAction.java @@ -0,0 +1,226 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import jakarta.persistence.Timeout; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.EffectiveEntityGraph; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; +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.internal.JdbcSelectWithActions; +import org.hibernate.sql.exec.spi.ExecutionContext; +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; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import static org.hibernate.sql.exec.SqlExecLogger.SQL_EXEC_LOGGER; +import static org.hibernate.sql.exec.internal.lock.LockingHelper.segmentLoadedValues; + +/** + * PostAction intended to perform collection locking with + * {@linkplain Locking.Scope#INCLUDE_COLLECTIONS} for Dialects + * which support "table hint locking" (T-SQL variants). + * + * @author Steve Ebersole + */ +public class CollectionLockingAction implements PostAction { + private final LoadedValuesCollectorImpl loadedValuesCollector; + private final LockMode lockMode; + private final Timeout lockTimeout; + + private CollectionLockingAction( + LoadedValuesCollectorImpl loadedValuesCollector, + LockMode lockMode, + Timeout lockTimeout) { + this.loadedValuesCollector = loadedValuesCollector; + this.lockMode = lockMode; + this.lockTimeout = lockTimeout; + } + + public static void apply( + LockOptions lockOptions, + QuerySpec lockingTarget, + JdbcSelectWithActions.Builder jdbcSelectBuilder) { + assert lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS; + + final var loadedValuesCollector = resolveLoadedValuesCollector( lockingTarget.getFromClause() ); + + // NOTE: we need to set this separately so that it can get incorporated into + // the JdbcValuesSourceProcessingState for proper callbacks + jdbcSelectBuilder.setLoadedValuesCollector( loadedValuesCollector ); + + // additionally, add a post-action which uses the collected values. + jdbcSelectBuilder.appendPostAction( new CollectionLockingAction( + loadedValuesCollector, + lockOptions.getLockMode(), + lockOptions.getTimeout() + ) ); + } + + @Override + public void performPostAction( + StatementAccess jdbcStatementAccess, + Connection jdbcConnection, + ExecutionContext executionContext) { + LockingHelper.logLoadedValues( loadedValuesCollector ); + + final SharedSessionContractImplementor session = executionContext.getSession(); + + // NOTE: we deal with effective graphs here to make sure embedded associations are treated as lazy + final EffectiveEntityGraph effectiveEntityGraph = session.getLoadQueryInfluencers().getEffectiveEntityGraph(); + final RootGraphImplementor initialGraph = effectiveEntityGraph.getGraph(); + final GraphSemantic initialSemantic = effectiveEntityGraph.getSemantic(); + + // collect registrations by entity type + final Map> entitySegments = segmentLoadedValues( loadedValuesCollector ); + + try { + // for each entity-type, prepare a locking select statement per table. + // this is based on the attributes for "state array" ordering purposes - + // we match each attribute to the table it is mapped to and add it to + // the select-list for that table-segment. + entitySegments.forEach( (entityMappingType, entityKeys) -> { + if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { + SQL_EXEC_LOGGER.debugf( "Starting include-collections locking process - %s", + entityMappingType.getEntityName() ); + } + + // apply an empty "fetch graph" to make sure any embedded associations reachable from + // any of the DomainResults we will create are treated as lazy + final RootGraphImplementor graph = entityMappingType.createRootGraph( session ); + effectiveEntityGraph.clear(); + effectiveEntityGraph.applyGraph( graph, GraphSemantic.FETCH ); + + // create a cross-reference of information related to an entity based on its identifier. + // we use this as the collection owners whose collections need to be locked + final Map entityDetailsMap = LockingHelper.resolveEntityKeys( entityKeys, executionContext ); + + SqmMutationStrategyHelper.visitCollectionTables( entityMappingType, (attribute) -> { + // we may need to lock the "collection table". + // the conditions are a bit unclear as to directionality, etc., so for now lock each. + LockingHelper.lockCollectionTable( + attribute, + lockMode, + lockTimeout, + entityDetailsMap, + executionContext + ); + } ); + } ); + } + finally { + // reset the effective graph to whatever it was when we started + effectiveEntityGraph.clear(); + session.getLoadQueryInfluencers().applyEntityGraph( initialGraph, initialSemantic ); + } + } + + private static LoadedValuesCollectorImpl resolveLoadedValuesCollector(FromClause fromClause) { + final List roots = fromClause.getRoots(); + if ( roots.size() == 1 ) { + return new LoadedValuesCollectorImpl( + List.of( roots.get( 0 ).getNavigablePath() ) + ); + } + else { + return new LoadedValuesCollectorImpl( + roots.stream().map( TableGroup::getNavigablePath ).toList() + ); + } + } + + private static Map> segmentLoadedValues(LoadedValuesCollector loadedValuesCollector) { + final Map> map = new IdentityHashMap<>(); + LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map ); + LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), 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. + } + return map; + } + + private static class LoadedValuesCollectorImpl implements LoadedValuesCollector { + private final List rootPaths; + + private List rootEntitiesToLock; + private List nonRootEntitiesToLock; + private List collectionsToLock; + + private LoadedValuesCollectorImpl(List rootPaths) { + this.rootPaths = rootPaths; + } + + @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<>(); + } + nonRootEntitiesToLock.add( new LoadedEntityRegistration( navigablePath, entityDescriptor, entityKey ) ); + } + } + + @Override + public void registerCollection(NavigablePath navigablePath, PluralAttributeMapping collectionDescriptor, CollectionKey collectionKey) { + if ( collectionsToLock == null ) { + collectionsToLock = new ArrayList<>(); + } + collectionsToLock.add( new LoadedCollectionRegistration( navigablePath, collectionDescriptor, collectionKey ) ); + } + + @Override + public void clear() { + if ( rootEntitiesToLock != null ) { + rootEntitiesToLock.clear(); + } + if ( nonRootEntitiesToLock != null ) { + nonRootEntitiesToLock.clear(); + } + if ( collectionsToLock != null ) { + collectionsToLock.clear(); + } + } + + @Override + public List getCollectedRootEntities() { + return rootEntitiesToLock; + } + + @Override + public List getCollectedNonRootEntities() { + return nonRootEntitiesToLock; + } + + @Override + public List getCollectedCollections() { + return collectionsToLock; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/EntityDetails.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/EntityDetails.java new file mode 100644 index 000000000000..e421ec2e519f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/EntityDetails.java @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; + +/** + * Models details about an entity instance used while performing follow-on locking + * + * @author Steve Ebersole + */ +public record EntityDetails(EntityKey key, EntityEntry entry, Object instance) { +} 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 new file mode 100644 index 000000000000..48ba589f1ad1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java @@ -0,0 +1,330 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import jakarta.persistence.Timeout; +import org.hibernate.AssertionFailure; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.Timeouts; +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.EffectiveEntityGraph; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.persister.entity.UnionSubclassEntityPersister; +import org.hibernate.query.internal.QueryOptionsImpl; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; +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.internal.JdbcSelectWithActions; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.StatementAccess; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; +import org.hibernate.sql.exec.spi.PostAction; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.hibernate.sql.exec.SqlExecLogger.SQL_EXEC_LOGGER; + +/** + * PostAction for a {@linkplain org.hibernate.sql.exec.internal.JdbcSelectWithActions} which + * performs follow-on locking based on the loaded values. + * + * @implSpec Relies on the fact that {@linkplain LoadedValuesCollector} has + * already applied filtering for things which actually need locked. + * + * @author Steve Ebersole + */ +public class FollowOnLockingAction implements PostAction { + private final LoadedValuesCollectorImpl loadedValuesCollector; + private final LockMode lockMode; + private final Timeout lockTimeout; + private final Locking.Scope lockScope; + + private FollowOnLockingAction( + LoadedValuesCollectorImpl loadedValuesCollector, + LockMode lockMode, + Timeout lockTimeout, + Locking.Scope lockScope) { + this.loadedValuesCollector = loadedValuesCollector; + this.lockMode = lockMode; + this.lockTimeout = lockTimeout; + this.lockScope = lockScope; + } + + public static void apply( + LockOptions lockOptions, + QuerySpec lockingTarget, + LockingClauseStrategy lockingClauseStrategy, + JdbcSelectWithActions.Builder jdbcSelectBuilder) { + final FromClause fromClause = lockingTarget.getFromClause(); + final var loadedValuesCollector = resolveLoadedValuesCollector( fromClause, lockingClauseStrategy ); + + // NOTE: we need to set this separately so that it can get incorporated into + // the JdbcValuesSourceProcessingState for proper callbacks + jdbcSelectBuilder.setLoadedValuesCollector( loadedValuesCollector ); + + // additionally, add a post-action which uses the collected values. + jdbcSelectBuilder.appendPostAction( new FollowOnLockingAction( + loadedValuesCollector, + lockOptions.getLockMode(), + lockOptions.getTimeout(), + lockOptions.getScope() + ) ); + } + + @Override + public void performPostAction( + StatementAccess jdbcStatementAccess, + Connection jdbcConnection, + ExecutionContext executionContext) { + LockingHelper.logLoadedValues( loadedValuesCollector ); + + final SharedSessionContractImplementor session = executionContext.getSession(); + + // NOTE: we deal with effective graphs here to make sure embedded associations are treated as lazy + final EffectiveEntityGraph effectiveEntityGraph = session.getLoadQueryInfluencers().getEffectiveEntityGraph(); + final RootGraphImplementor initialGraph = effectiveEntityGraph.getGraph(); + final GraphSemantic initialSemantic = effectiveEntityGraph.getSemantic(); + + try { + // collect registrations by entity type + final Map> entitySegments = segmentLoadedValues(); + + // for each entity-type, prepare a locking select statement per table. + // this is based on the attributes for "state array" ordering purposes - + // we match each attribute to the table it is mapped to and add it to + // the select-list for that table-segment. + entitySegments.forEach( (entityMappingType, entityKeys) -> { + if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { + SQL_EXEC_LOGGER.debugf( "Starting follow-on locking process - %s", entityMappingType.getEntityName() ); + } + + // apply an empty "fetch graph" to make sure any embedded associations reachable from + // any of the DomainResults we will create are treated as lazy + final RootGraphImplementor graph = entityMappingType.createRootGraph( session ); + effectiveEntityGraph.clear(); + effectiveEntityGraph.applyGraph( graph, GraphSemantic.FETCH ); + + // create a table-lock reference for each table for the entity (keyed by name) + final Map tableLocks = prepareTableLocks( entityMappingType, entityKeys, session ); + + // create a cross-reference of information related to an entity based on its identifier, + // we'll use this later when we adjust the state array and inject state into the entity instance. + final Map entityDetailsMap = LockingHelper.resolveEntityKeys( entityKeys, executionContext ); + + entityMappingType.forEachAttributeMapping( (index, attributeMapping) -> { + if ( attributeMapping instanceof PluralAttributeMapping pluralAttributeMapping ) { + // we need to handle collections specially (which we do below, so skip them here) + return; + } + + final TableLock tableLock = resolveTableLock( attributeMapping, tableLocks, entityMappingType ); + + if ( tableLock == null ) { + throw new AssertionFailure( String.format( + Locale.ROOT, + "Unable to locate table for attribute `%s`", + attributeMapping.getNavigableRole().getFullPath() + ) ); + } + + // here we apply the selection for the attribute to the corresponding table-lock ref + tableLock.applyAttribute( index, attributeMapping ); + } ); + + // now we do process any collections, if asked + if ( lockScope == Locking.Scope.INCLUDE_COLLECTIONS ) { + SqmMutationStrategyHelper.visitCollectionTables( entityMappingType, (attribute) -> { + // we may need to lock the "collection table". + // the conditions are a bit unclear as to directionality, etc., so for now lock each. + LockingHelper.lockCollectionTable( + attribute, + lockMode, + lockTimeout, + entityDetailsMap, + executionContext + ); + } ); + } + + + // at this point, we have all the individual locking selects ready to go - execute them + final QueryOptions lockingOptions = buildLockingOptions( executionContext ); + tableLocks.forEach( (s, tableLock) -> { + tableLock.performActions( entityDetailsMap, lockingOptions, session ); + } ); + } ); + } + finally { + // reset the effective graph to whatever it was when we started + effectiveEntityGraph.clear(); + session.getLoadQueryInfluencers().applyEntityGraph( initialGraph, initialSemantic ); + } + } + + private TableLock resolveTableLock( + AttributeMapping attributeMapping, + Map tableSegments, + EntityMappingType entityMappingType) { + if ( entityMappingType.getEntityPersister() instanceof UnionSubclassEntityPersister usp ) { + // in the union-subclass strategy, attributes defined on the super are reported as + // contained by the logical super table. See also the hacks in TableSegment + // to deal with this + // todo (JdbcOperation) : need to allow for secondary-tables + return tableSegments.get( usp.getMappedTableDetails().getTableName() ); + } + else { + return tableSegments.get( attributeMapping.getContainingTableExpression() ); + } + } + + private QueryOptions buildLockingOptions(ExecutionContext executionContext) { + final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl(); + lockingQueryOptions.getLockOptions().setLockMode( lockMode ); + lockingQueryOptions.getLockOptions().setTimeout( Timeouts.WAIT_FOREVER ); + lockingQueryOptions.getLockOptions().setFollowOnStrategy( Locking.FollowOn.DISALLOW ); + if ( executionContext.getQueryOptions().isReadOnly() == Boolean.TRUE ) { + lockingQueryOptions.setReadOnly( true ); + } + return lockingQueryOptions; + } + + private Map> segmentLoadedValues() { + final Map> map = new IdentityHashMap<>(); + LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map ); + LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map ); + if ( map.isEmpty() ) { + throw new AssertionFailure( "Expecting some values" ); + } + return map; + } + + private Map prepareTableLocks( + EntityMappingType entityMappingType, + List entityKeys, + SharedSessionContractImplementor session) { + final Map segments = new HashMap<>(); + entityMappingType.forEachTableDetails( (tableDetails) -> segments.put( + tableDetails.getTableName(), + createTableLock( tableDetails, entityMappingType, entityKeys, session ) + ) ); + return segments; + } + + private TableLock createTableLock(TableDetails tableDetails, EntityMappingType entityMappingType, List entityKeys, SharedSessionContractImplementor session) { + return new TableLock( tableDetails, entityMappingType, entityKeys, session ); + } + + private static LoadedValuesCollectorImpl resolveLoadedValuesCollector( + FromClause fromClause, + LockingClauseStrategy lockingClauseStrategy) { + final List roots = fromClause.getRoots(); + if ( roots.size() == 1 ) { + return new LoadedValuesCollectorImpl( + List.of( roots.get( 0 ).getNavigablePath() ), + lockingClauseStrategy + ); + } + else { + return new LoadedValuesCollectorImpl( + roots.stream().map( TableGroup::getNavigablePath ).toList(), + lockingClauseStrategy + ); + } + } + + public static class LoadedValuesCollectorImpl implements LoadedValuesCollector { + private final List rootPaths; + private final Collection pathsToLock; + + private List rootEntitiesToLock; + private List nonRootEntitiesToLock; + private List collectionsToLock; + + public LoadedValuesCollectorImpl(List rootPaths, LockingClauseStrategy lockingClauseStrategy) { + this.rootPaths = rootPaths; + pathsToLock = LockingHelper.extractPathsToLock( lockingClauseStrategy ); + } + + @Override + public void registerEntity(NavigablePath navigablePath, EntityMappingType entityDescriptor, EntityKey entityKey) { + if ( !pathsToLock.contains( navigablePath ) ) { + return; + } + + 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 ) ); + } + } + + @Override + public void registerCollection(NavigablePath navigablePath, PluralAttributeMapping collectionDescriptor, CollectionKey collectionKey) { + if ( !pathsToLock.contains( navigablePath ) ) { + return; + } + + if ( collectionsToLock == null ) { + collectionsToLock = new ArrayList<>(); + } + collectionsToLock.add( new LoadedCollectionRegistration( navigablePath, collectionDescriptor, collectionKey ) ); + } + + @Override + public void clear() { + if ( rootEntitiesToLock != null ) { + rootEntitiesToLock.clear(); + } + if ( nonRootEntitiesToLock != null ) { + nonRootEntitiesToLock.clear(); + } + if ( collectionsToLock != null ) { + collectionsToLock.clear(); + } + } + + @Override + public List getCollectedRootEntities() { + return rootEntitiesToLock; + } + + @Override + public List getCollectedNonRootEntities() { + return nonRootEntitiesToLock; + } + + @Override + public List getCollectedCollections() { + return collectionsToLock; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingCreationStates.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingCreationStates.java new file mode 100644 index 000000000000..ace41c64b389 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingCreationStates.java @@ -0,0 +1,236 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import org.hibernate.LockMode; +import org.hibernate.engine.FetchTiming; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.query.results.internal.FromClauseAccessImpl; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; +import org.hibernate.sql.ast.spi.SqlAstCreationContext; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlAstProcessingState; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.Fetchable; +import org.hibernate.sql.results.graph.FetchableContainer; +import org.hibernate.sql.results.graph.internal.ImmutableFetchList; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Used by {@linkplain TableLock} in creating the SQL AST and DomainResults + * as part of its follow-on lock handling. + * + * @author Steve Ebersole + */ +public class LockingCreationStates + implements DomainResultCreationState, SqlAstCreationState, SqlAstProcessingState, SqlExpressionResolver { + + private final QuerySpec querySpec; + private final SessionFactoryImplementor sessionFactory; + + private final FromClauseAccessImpl fromClauseAccess; + private final SqlAliasBaseManager sqlAliasBaseManager; + + private final Map sqlExpressionMap = new HashMap<>(); + private final Map sqlSelectionMap = new HashMap<>(); + + public LockingCreationStates( + QuerySpec querySpec, + TableGroup root, + SessionFactoryImplementor sessionFactory) { + this.querySpec = querySpec; + this.sessionFactory = sessionFactory; + + fromClauseAccess = new FromClauseAccessImpl(); + fromClauseAccess.registerTableGroup( root.getNavigablePath(), root ); + + sqlAliasBaseManager = new SqlAliasBaseManager(); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // DomainResultCreationState + + @Override + public FromClauseAccessImpl getFromClauseAccess() { + return fromClauseAccess; + } + + @Override + public SqlAliasBaseManager getSqlAliasBaseManager() { + return sqlAliasBaseManager; + } + + @Override + public LockingCreationStates getSqlAstCreationState() { + return this; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SqlAstCreationState + + @Override + public SqlAstCreationContext getCreationContext() { + return sessionFactory.getSqlTranslationEngine(); + } + + @Override + public LockingCreationStates getCurrentProcessingState() { + return this; + } + + @Override + public LockingCreationStates getSqlExpressionResolver() { + return getCurrentProcessingState(); + } + + @Override + public SqlAliasBaseGenerator getSqlAliasBaseGenerator() { + return sqlAliasBaseManager; + } + + @Override + public LoadQueryInfluencers getLoadQueryInfluencers() { + return null; + } + + @Override + public boolean applyOnlyLoadByKeyFilters() { + return true; + } + + @Override + public void registerLockMode(String identificationVariable, LockMode explicitLockMode) { + throw new UnsupportedOperationException(); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SqlAstProcessingState + + @Override + public SqlAstProcessingState getParentState() { + return null; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SqlExpressionResolver + + @Override + public Expression resolveSqlExpression( + ColumnReferenceKey key, + Function creator) { + final Expression expression = sqlExpressionMap.get( key ); + if ( expression != null ) { + return expression; + } + + final Expression created = creator.apply( this ); + sqlExpressionMap.put( key, created ); + return created; + } + + @Override + public SqlSelection resolveSqlSelection( + Expression expression, + JavaType javaType, + FetchParent fetchParent, + TypeConfiguration typeConfiguration) { + final SqlSelection sqlSelection = sqlSelectionMap.get( expression ); + if ( sqlSelection != null ) { + return sqlSelection; + } + + if ( expression instanceof ColumnReference columnReference ) { + final SqlSelectionImpl created = new SqlSelectionImpl( columnReference, querySpec.getSelectClause().getSqlSelections().size() ); + sqlSelectionMap.put( expression, created ); + querySpec.getSelectClause().addSqlSelection( created ); + return created; + } + + throw new UnsupportedOperationException( "Unsupported Expression type (expected ColumnReference) : " + expression ); + } + + @Override + public ModelPart resolveModelPart(NavigablePath navigablePath) { + return null; + } + + @Override + public ImmutableFetchList visitFetches(FetchParent fetchParent) { + final ImmutableFetchList.Builder fetches = + new ImmutableFetchList.Builder( fetchParent.getReferencedMappingContainer() ); + + final FetchableContainer referencedMappingContainer = fetchParent.getReferencedMappingContainer(); + + final int size = referencedMappingContainer.getNumberOfFetchables(); + for ( int i = 0; i < size; i++ ) { + final Fetchable fetchable = referencedMappingContainer.getFetchable( i ); + processFetchable( fetchParent, fetchable, fetches ); + } + return fetches.build(); + + } + + private void processFetchable(FetchParent fetchParent, Fetchable fetchable, ImmutableFetchList.Builder fetches) { + if ( !fetchable.isSelectable() ) { + return; + } + + final NavigablePath fetchablePath = fetchParent.resolveNavigablePath( fetchable ); + + final Fetch fetch = fetchParent.generateFetchableFetch( + fetchable, + fetchablePath, + FetchTiming.DELAYED, + false, + null, + this + ); + + fetches.add( fetch ); + } + + @Override + public R withNestedFetchParent(FetchParent fetchParent, Function action) { + return null; + } + + @Override + public boolean isResolvingCircularFetch() { + return false; + } + + @Override + public void setResolvingCircularFetch(boolean resolvingCircularFetch) { + + } + + @Override + public ForeignKeyDescriptor.Nature getCurrentlyResolvingForeignKeyPart() { + return null; + } + + @Override + public void setCurrentlyResolvingForeignKeyPart(ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide) { + + } +} 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 new file mode 100644 index 000000000000..866676bfd89e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingHelper.java @@ -0,0 +1,427 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import jakarta.persistence.Timeout; +import org.hibernate.LockMode; +import org.hibernate.ScrollMode; +import org.hibernate.Session; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.ModelPartContainer; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; +import org.hibernate.query.internal.QueryOptionsImpl; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.LockingClauseStrategy; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.InListPredicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.StandardStatementCreator; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelectExecutor; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; +import org.hibernate.sql.results.spi.ListResultsConsumer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +import static org.hibernate.sql.exec.SqlExecLogger.SQL_EXEC_LOGGER; + +/** + * Helper for dealing with follow-on locking for collection-tables. + * + * @author Steve Ebersole + */ +public class LockingHelper { + /** + * Lock a collection-table for a single entity. + * + * @param attributeMapping The plural attribute whose table needs locked. + * @param lockMode The lock mode to apply + * @param lockTimeout A lock timeout to apply, if one. + * @param collectionToLock The collection to lock. + * + * @see Session#lock + */ + public static void lockCollectionTable( + PluralAttributeMapping attributeMapping, + LockMode lockMode, + Timeout lockTimeout, + PersistentCollection collectionToLock, + ExecutionContext executionContext) { + final SharedSessionContractImplementor session = executionContext.getSession(); + + final ForeignKeyDescriptor keyDescriptor = attributeMapping.getKeyDescriptor(); + final String keyTableName = keyDescriptor.getKeyTable(); + + if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { + SQL_EXEC_LOGGER.debugf( "Collection locking for collection table `%s` - %s", keyTableName, attributeMapping.getRootPathName() ); + } + + final QuerySpec querySpec = new QuerySpec( true ); + + final NamedTableReference tableReference = new NamedTableReference( keyTableName, "tbl" ); + final LockingTableGroup tableGroup = new LockingTableGroup( + tableReference, + keyTableName, + attributeMapping, + keyDescriptor.getKeySide().getModelPart() + ); + + querySpec.getFromClause().addRoot( tableGroup ); + + final ValuedModelPart keyPart = keyDescriptor.getKeyPart(); + final ColumnReference columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) ); + + // NOTE: We add the key column to the selection list, but never create a DomainResult + // as we won't read the value back. Ideally, we would read the "value column(s)" and + // update the collection state accordingly much like is done for entity state - + // however, the concern is minor, so for simplicity we do not. + final SqlSelectionImpl sqlSelection = new SqlSelectionImpl( columnReference, 0 ); + querySpec.getSelectClause().addSqlSelection( sqlSelection ); + + final JdbcParameterBindingsImpl parameterBindings = new JdbcParameterBindingsImpl( keyDescriptor.getJdbcTypeCount() ); + + final ComparisonPredicate restriction; + if ( keyDescriptor.getJdbcTypeCount() == 1 ) { + final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( keyPart.getSelectable( 0 ).getJdbcMapping() ); + keyDescriptor.breakDownJdbcValues( + collectionToLock.getKey(), + (valueIndex, value, jdbcValueMapping) -> { + parameterBindings.addBinding( + jdbcParameter, + new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value ) + ); + }, + session + ); + restriction = new ComparisonPredicate( columnReference, ComparisonOperator.EQUAL, jdbcParameter ); + } + else { + final List columnReferences = new ArrayList<>( keyDescriptor.getJdbcTypeCount() ); + final List jdbcParameters = new ArrayList<>( keyDescriptor.getJdbcTypeCount() ); + keyDescriptor.breakDownJdbcValues( + collectionToLock.getKey(), + (valueIndex, value, jdbcValueMapping) -> { + columnReferences.add( new ColumnReference( tableReference, jdbcValueMapping ) ); + + final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() ); + jdbcParameters.add( jdbcParameter ); + parameterBindings.addBinding( + jdbcParameter, + new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value ) + ); + }, + session + ); + final SqlTuple columns = new SqlTuple( columnReferences, keyDescriptor ); + final SqlTuple parameters = new SqlTuple( jdbcParameters, keyDescriptor ); + restriction = new ComparisonPredicate( columns, ComparisonOperator.EQUAL, parameters ); + } + querySpec.applyPredicate( restriction ); + + final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl(); + lockingQueryOptions.getLockOptions().setLockMode( lockMode ); + lockingQueryOptions.getLockOptions().setTimeout( lockTimeout ); + final ExecutionContext lockingExecutionContext = new BaseExecutionContext( executionContext.getSession() ) { + @Override + public QueryOptions getQueryOptions() { + return lockingQueryOptions; + } + }; + + performLocking( querySpec, parameterBindings, lockingExecutionContext ); + } + + /** + * Lock a collection-table. + * + * @param attributeMapping The plural attribute whose table needs locked. + * @param lockMode The lock mode to apply + * @param lockTimeout A lock timeout to apply, if one. + * @param ownerDetailsMap Details for each owner, whose collection-table rows should be locked. + */ + public static void lockCollectionTable( + PluralAttributeMapping attributeMapping, + LockMode lockMode, + Timeout lockTimeout, + Map ownerDetailsMap, + ExecutionContext executionContext) { + final ForeignKeyDescriptor keyDescriptor = attributeMapping.getKeyDescriptor(); + final String keyTableName = keyDescriptor.getKeyTable(); + + if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { + SQL_EXEC_LOGGER.debugf( "Follow-on locking for collection table `%s` - %s", keyTableName, attributeMapping.getRootPathName() ); + } + + final QuerySpec querySpec = new QuerySpec( true ); + + final NamedTableReference tableReference = new NamedTableReference( keyTableName, "tbl" ); + final LockingTableGroup tableGroup = new LockingTableGroup( + tableReference, + keyTableName, + attributeMapping, + keyDescriptor.getKeySide().getModelPart() + ); + + querySpec.getFromClause().addRoot( tableGroup ); + + final ValuedModelPart keyPart = keyDescriptor.getKeyPart(); + final ColumnReference columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) ); + + // NOTE: We add the key column to the selection list, but never create a DomainResult + // as we won't read the value back. Ideally, we would read the "value column(s)" and + // update the collection state accordingly much like is done for entity state - + // however, the concern is minor, so for simplicity we do not. + final SqlSelectionImpl sqlSelection = new SqlSelectionImpl( columnReference, 0 ); + querySpec.getSelectClause().addSqlSelection( sqlSelection ); + + final int expectedParamCount = ownerDetailsMap.size() * keyDescriptor.getJdbcTypeCount(); + final JdbcParameterBindingsImpl parameterBindings = new JdbcParameterBindingsImpl( expectedParamCount ); + + final InListPredicate restriction; + if ( keyDescriptor.getJdbcTypeCount() == 1 ) { + restriction = new InListPredicate( columnReference ); + applySimpleCollectionKeyTableLockRestrictions( + attributeMapping, + keyDescriptor, + restriction, + parameterBindings, + ownerDetailsMap, + executionContext.getSession() + ); + } + else { + restriction = applyCompositeCollectionKeyTableLockRestrictions( + attributeMapping, + keyDescriptor, + tableReference, + parameterBindings, + ownerDetailsMap, + executionContext.getSession() + ); + } + querySpec.applyPredicate( restriction ); + + final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl(); + lockingQueryOptions.getLockOptions().setLockMode( lockMode ); + lockingQueryOptions.getLockOptions().setTimeout( lockTimeout ); + final ExecutionContext lockingExecutionContext = new BaseExecutionContext( executionContext.getSession() ) { + @Override + public QueryOptions getQueryOptions() { + return lockingQueryOptions; + } + }; + + performLocking( querySpec, parameterBindings, lockingExecutionContext ); + } + + private static void applySimpleCollectionKeyTableLockRestrictions( + PluralAttributeMapping attributeMapping, + ForeignKeyDescriptor keyDescriptor, + InListPredicate restriction, + JdbcParameterBindingsImpl parameterBindings, + Map ownerDetailsMap, + SharedSessionContractImplementor session) { + + ownerDetailsMap.forEach( (o, entityDetails) -> { + final PersistentCollection collectionInstance = (PersistentCollection) entityDetails.entry().getLoadedState()[attributeMapping.getStateArrayPosition()]; + final Object collectionKeyValue = collectionInstance.getKey(); + keyDescriptor.breakDownJdbcValues( + collectionKeyValue, + (valueIndex, value, jdbcValueMapping) -> { + final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( + jdbcValueMapping.getJdbcMapping() ); + restriction.addExpression( jdbcParameter ); + + parameterBindings.addBinding( + jdbcParameter, + new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value ) + ); + }, + session + ); + } ); + } + + private static InListPredicate applyCompositeCollectionKeyTableLockRestrictions( + PluralAttributeMapping attributeMapping, + ForeignKeyDescriptor keyDescriptor, + TableReference tableReference, + JdbcParameterBindingsImpl parameterBindings, + Map ownerDetailsMap, + SharedSessionContractImplementor session) { + if ( !session.getDialect().supportsRowValueConstructorSyntaxInInList() ) { + // for now... + throw new UnsupportedOperationException( + "Follow-on collection-table locking with composite keys is not supported for Dialects" + + " which do not support tuples (row constructor syntax) as part of an in-list" + ); + } + + final List columnReferences = new ArrayList<>( keyDescriptor.getJdbcTypeCount() ); + keyDescriptor.forEachSelectable( (selectionIndex, selectableMapping) -> { + columnReferences.add( new ColumnReference( tableReference, selectableMapping ) ); + } ); + final InListPredicate inListPredicate = new InListPredicate( new SqlTuple( columnReferences, keyDescriptor ) ); + + ownerDetailsMap.forEach( (o, entityDetails) -> { + final PersistentCollection collectionInstance = (PersistentCollection) entityDetails.entry().getLoadedState()[attributeMapping.getStateArrayPosition()]; + final Object collectionKeyValue = collectionInstance.getKey(); + + final List jdbcParameters = new ArrayList<>( keyDescriptor.getJdbcTypeCount() ); + keyDescriptor.breakDownJdbcValues( + collectionKeyValue, + (valueIndex, value, jdbcValueMapping) -> { + final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() ); + jdbcParameters.add( jdbcParameter ); + parameterBindings.addBinding( + jdbcParameter, + new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value ) + ); + }, + session + ); + inListPredicate.addExpression( new SqlTuple( jdbcParameters, keyDescriptor ) ); + } ); + + return inListPredicate; + } + + private static void performLocking( + QuerySpec querySpec, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext lockingExecutionContext) { + final SessionFactoryImplementor sessionFactory = lockingExecutionContext.getSession().getSessionFactory(); + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + + final SelectStatement selectStatement = new SelectStatement( querySpec ); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcServices.getDialect().getSqlAstTranslatorFactory(); + final SqlAstTranslator translator = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory, selectStatement ); + final JdbcOperationQuerySelect jdbcOperation = translator.translate( jdbcParameterBindings, lockingExecutionContext.getQueryOptions() ); + + final JdbcSelectExecutor jdbcSelectExecutor = jdbcServices.getJdbcSelectExecutor(); + jdbcSelectExecutor.executeQuery( + jdbcOperation, + jdbcParameterBindings, + lockingExecutionContext, + row -> row, + Object[].class, + StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ), + ListResultsConsumer.instance( ListResultsConsumer.UniqueSemantic.ALLOW ) + ); + } + + /** + * Log information about the entries in LoadedValuesCollector. + */ + public static void logLoadedValues(LoadedValuesCollector collector) { + if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { + SQL_EXEC_LOGGER.debug( "Follow-on locking collected loaded values..." ); + + SQL_EXEC_LOGGER.debug( " Loaded root entities:" ); + collector.getCollectedRootEntities().forEach( (reg) -> { + SQL_EXEC_LOGGER.debugf( " - %s#%s", reg.entityDescriptor().getEntityName(), reg.entityKey().getIdentifier() ); + } ); + + SQL_EXEC_LOGGER.debug( " Loaded non-root entities:" ); + collector.getCollectedNonRootEntities().forEach( (reg) -> { + SQL_EXEC_LOGGER.debugf( " - %s#%s", reg.entityDescriptor().getEntityName(), reg.entityKey().getIdentifier() ); + } ); + + SQL_EXEC_LOGGER.debug( " Loaded collections:" ); + collector.getCollectedCollections().forEach( (reg) -> { + SQL_EXEC_LOGGER.debugf( " - %s#%s", reg.collectionDescriptor().getRootPathName(), reg.collectionKey().getKey() ); + } ); + } + } + + /** + * Extracts all NavigablePaths to lock based on the {@linkplain LockingClauseStrategy locking strategy} + * from the {@linkplain SqlAstTranslator SQL AST translator}. + */ + public static Collection extractPathsToLock(LockingClauseStrategy lockingClauseStrategy) { + final LinkedHashSet paths = new LinkedHashSet<>(); + + final Collection rootsToLock = lockingClauseStrategy.getRootsToLock(); + if ( rootsToLock != null ) { + rootsToLock.forEach( (tableGroup) -> paths.add( tableGroup.getNavigablePath() ) ); + } + + final Collection joinsToLock = lockingClauseStrategy.getJoinsToLock(); + if ( joinsToLock != null ) { + joinsToLock.forEach( (tableGroupJoin) -> { + paths.add( tableGroupJoin.getNavigablePath() ); + + final ModelPartContainer modelPart = tableGroupJoin.getJoinedGroup().getModelPart(); + if ( modelPart instanceof PluralAttributeMapping pluralAttributeMapping ) { + final NavigablePath elementPath = tableGroupJoin.getNavigablePath().append( pluralAttributeMapping.getElementDescriptor().getPartName() ); + paths.add( elementPath ); + + if ( pluralAttributeMapping.getIndexDescriptor() != null ) { + final NavigablePath indexPath = tableGroupJoin.getNavigablePath().append( pluralAttributeMapping.getIndexDescriptor().getPartName() ); + paths.add( indexPath ); + } + } + } ); + } + return paths; + } + + public static void segmentLoadedValues(List registrations, Map> map) { + if ( registrations == null ) { + return; + } + + registrations.forEach( (registration) -> { + final List entityKeys = map.computeIfAbsent( + registration.entityDescriptor(), + entityMappingType -> new ArrayList<>() + ); + entityKeys.add( registration.entityKey() ); + } ); + } + + public static Map resolveEntityKeys(List entityKeys, ExecutionContext executionContext) { + final Map map = new HashMap<>(); + final PersistenceContext persistenceContext = executionContext.getSession().getPersistenceContext(); + entityKeys.forEach( (entityKey) -> { + final Object instance = persistenceContext.getEntity( entityKey ); + final EntityEntry entry = persistenceContext.getEntry( instance ); + map.put( entityKey.getIdentifierValue(), new EntityDetails( entityKey, entry, instance ) ); + } ); + return map; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingTableGroup.java new file mode 100644 index 000000000000..60d64c7c16e8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingTableGroup.java @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.ModelPartContainer; +import org.hibernate.metamodel.mapping.SelectableMappings; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * TableGroup wrapping a {@linkplain TableLock table to be locked}. + * + * @author Steve Ebersole + */ +public class LockingTableGroup implements TableGroup { + private final TableReference tableReference; + private final String tableName; + private final ModelPartContainer modelPart; + private final NavigablePath navigablePath; + private final SelectableMappings keyColumnMappings; + + private List tableGroupJoins; + + public LockingTableGroup( + TableReference tableReference, + String tableName, + ModelPartContainer modelPart, + SelectableMappings keyColumnMappings) { + this.tableReference = tableReference; + this.tableName = tableName; + this.modelPart = modelPart; + this.navigablePath = new NavigablePath( tableName ); + this.keyColumnMappings = keyColumnMappings; + } + + public SelectableMappings getKeyColumnMappings() { + return keyColumnMappings; + } + + @Override + public NavigablePath getNavigablePath() { + return navigablePath; + } + + @Override + public String getGroupAlias() { + return ""; + } + + @Override + public ModelPartContainer getModelPart() { + return modelPart; + } + + @Override + public String getSourceAlias() { + return ""; + } + + @Override + public List getTableGroupJoins() { + return tableGroupJoins == null + ? Collections.emptyList() + : tableGroupJoins; + } + + @Override + public List getNestedTableGroupJoins() { + return List.of(); + } + + @Override + public boolean canUseInnerJoins() { + return true; + } + + @Override + public void addTableGroupJoin(TableGroupJoin join) { + if ( join.getJoinedGroup().isRealTableGroup() ) { + throw new UnsupportedOperationException(); + } + + if ( tableGroupJoins == null ) { + tableGroupJoins = new ArrayList<>(); + } + tableGroupJoins.add( join ); + } + + @Override + public void prependTableGroupJoin(NavigablePath navigablePath, TableGroupJoin join) { + addTableGroupJoin( join ); + } + + @Override + public void addNestedTableGroupJoin(TableGroupJoin join) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitTableGroupJoins(Consumer consumer) { + if ( tableGroupJoins != null ) { + tableGroupJoins.forEach( consumer ); + } + } + + @Override + public void visitNestedTableGroupJoins(Consumer consumer) { + } + + @Override + public void applyAffectedTableNames(Consumer nameCollector) { + nameCollector.accept( tableName ); + } + + @Override + public TableReference getPrimaryTableReference() { + return tableReference; + } + + @Override + public List getTableReferenceJoins() { + return List.of(); + } + + @Override + public ModelPart getExpressionType() { + return modelPart; + } + + @Override + public TableReference getTableReference(NavigablePath navigablePath, String tableExpression, boolean resolve) { + if ( tableName.equals( tableExpression ) ) { + return tableReference; + } + return null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/SqlSelectionImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/SqlSelectionImpl.java new file mode 100644 index 000000000000..0e063c2dda12 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/SqlSelectionImpl.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.spi.SqlExpressionAccess; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; +import org.hibernate.type.descriptor.ValueExtractor; + +/** + * SqlSelection implementation used in follow-on locking. + * + * @author Steve Ebersole + */ +public class SqlSelectionImpl implements SqlSelection, SqlExpressionAccess { + private final ColumnReference columnReference; + private final int valuesArrayPosition; + + public SqlSelectionImpl(ColumnReference columnReference, int valuesArrayPosition) { + this.columnReference = columnReference; + this.valuesArrayPosition = valuesArrayPosition; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SqlExpressionAccess + + @Override + public Expression getSqlExpression() { + return columnReference; + } + + @Override + public ValueExtractor getJdbcValueExtractor() { + return columnReference.getJdbcMapping().getJdbcValueExtractor(); + } + + @Override + public int getValuesArrayPosition() { + return valuesArrayPosition; + } + + @Override + public Expression getExpression() { + return columnReference; + } + + @Override + public JdbcMappingContainer getExpressionType() { + return columnReference.getExpressionType(); + } + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public void accept(SqlAstWalker sqlAstWalker) { + sqlAstWalker.visitSqlSelection( this ); + } + + @Override + public SqlSelection resolve(JdbcValuesMetadata jdbcResultsMetadata, SessionFactoryImplementor sessionFactory) { + return null; + } +} 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 new file mode 100644 index 000000000000..374a18eab487 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableLock.java @@ -0,0 +1,340 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal.lock; + +import org.hibernate.AssertionFailure; +import org.hibernate.ScrollMode; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.persister.entity.UnionSubclassEntityPersister; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.UnionTableGroup; +import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.predicate.InListPredicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.StandardStatementCreator; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; +import org.hibernate.sql.exec.spi.JdbcSelectExecutor; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.spi.ListResultsConsumer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; +import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; +import static org.hibernate.sql.exec.SqlExecLogger.SQL_EXEC_LOGGER; + + +/** + * Models a single table for which to apply locking. + * + * @author Steve Ebersole + */ +public class TableLock { + private final TableDetails tableDetails; + private final EntityMappingType entityMappingType; + + private final QuerySpec querySpec = new QuerySpec( true ); + + private final NavigablePath rootPath; + + private final TableReference physicalTableReference; + private final TableGroup physicalTableGroup; + + private final TableReference logicalTableReference; + private final TableGroup logicalTableGroup; + + private final LockingCreationStates creationStates; + + private final List resultHandlers = new ArrayList<>(); + private final List> domainResults = new ArrayList<>(); + + private final JdbcParameterBindings jdbcParameterBindings; + + public TableLock( + TableDetails tableDetails, + EntityMappingType entityMappingType, + List entityKeys, + SharedSessionContractImplementor session) { + if ( SQL_EXEC_LOGGER.isDebugEnabled() ) { + SQL_EXEC_LOGGER.debugf( "Adding table `%s` for follow-on locking - %s", tableDetails.getTableName(), entityMappingType.getEntityName() ); + } + + this.tableDetails = tableDetails; + this.entityMappingType = entityMappingType; + this.rootPath = new NavigablePath( tableDetails.getTableName() ); + + this.physicalTableReference = new NamedTableReference( tableDetails.getTableName(), "tbl" ); + this.physicalTableGroup = new LockingTableGroup( physicalTableReference, tableDetails.getTableName(), entityMappingType, tableDetails.getKeyDetails() ); + + if ( entityMappingType.getEntityPersister() instanceof UnionSubclassEntityPersister usp ) { + final UnionTableReference unionTableReference = new UnionTableReference( + tableDetails.getTableName(), + usp.getSynchronizedQuerySpaces(), + "tbl", + false + ); + this.logicalTableReference = unionTableReference; + this.logicalTableGroup = new UnionTableGroup( + true, + rootPath, + unionTableReference, + usp, + null + ); + } + else { + logicalTableReference = physicalTableReference; + logicalTableGroup = physicalTableGroup; + } + + querySpec.getFromClause().addRoot( physicalTableGroup ); + + creationStates = new LockingCreationStates( + querySpec, + logicalTableGroup, + entityMappingType.getEntityPersister().getFactory() + ); + + // add the key as the first result + domainResults.add( tableDetails.getKeyDetails().createDomainResult( + rootPath.append( "{key}" ), + logicalTableReference, + null, + creationStates + ) ); + + final int expectedParamCount = entityKeys.size() * entityMappingType.getIdentifierMapping().getJdbcTypeCount(); + jdbcParameterBindings = new JdbcParameterBindingsImpl( expectedParamCount ); + + applyKeyRestrictions( entityKeys, session ); + } + + public void applyAttribute(int index, AttributeMapping attributeMapping) { + final NavigablePath attributePath = rootPath.append( attributeMapping.getPartName() ); + final DomainResult domainResult; + final ResultHandler resultHandler; + if ( attributeMapping instanceof ToOneAttributeMapping toOne ) { + domainResult = toOne.getForeignKeyDescriptor().getKeyPart().createDomainResult( + attributePath, + logicalTableGroup, + ForeignKeyDescriptor.PART_NAME, + creationStates + ); + resultHandler = new ToOneResultHandler( index, toOne ); + } + else { + domainResult = attributeMapping.createDomainResult( + attributePath, + logicalTableGroup, + null, + creationStates + ); + resultHandler = new NonToOneResultHandler( index ); + } + domainResults.add( domainResult ); + resultHandlers.add( resultHandler ); + } + + public void applyKeyRestrictions(List entityKeys, SharedSessionContractImplementor session) { + // todo (JdbcOperation) : Consider leveraging approach based on Dialect#useArrayForMultiValuedParameters + if ( entityMappingType.getIdentifierMapping().getJdbcTypeCount() == 1 ) { + applySimpleKeyRestriction( entityKeys, session ); + } + else { + applyCompositeKeyRestriction( entityKeys, session ); + } + } + + private void applySimpleKeyRestriction(List entityKeys, SharedSessionContractImplementor session) { + final EntityIdentifierMapping identifierMapping = entityMappingType.getIdentifierMapping(); + + final TableDetails.KeyColumn keyColumn = tableDetails.getKeyDetails().getKeyColumn( 0 ); + final ColumnReference columnReference = new ColumnReference( physicalTableReference, keyColumn ); + + final InListPredicate restriction = new InListPredicate( columnReference ); + querySpec.applyPredicate( restriction ); + + entityKeys.forEach( (entityKey) -> identifierMapping.breakDownJdbcValues( + entityKey.getIdentifierValue(), + (valueIndex, value, jdbcValueMapping) -> { + final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( + jdbcValueMapping.getJdbcMapping() ); + restriction.addExpression( jdbcParameter ); + + jdbcParameterBindings.addBinding( + jdbcParameter, + new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value ) + ); + }, + session + ) ); + } + + private void applyCompositeKeyRestriction(List entityKeys, SharedSessionContractImplementor session) { + final EntityIdentifierMapping identifierMapping = entityMappingType.getIdentifierMapping(); + + final ArrayList columnRefs = arrayList( tableDetails.getKeyDetails().getColumnCount() ); + tableDetails.getKeyDetails().forEachKeyColumn( (position, keyColumn) -> { + columnRefs.add( new ColumnReference( physicalTableReference, keyColumn ) ); + } ); + final SqlTuple keyRef = new SqlTuple( columnRefs, identifierMapping ); + + final InListPredicate restriction = new InListPredicate( keyRef ); + querySpec.applyPredicate( restriction ); + + entityKeys.forEach( (entityKey) -> { + final List valueParams = arrayList( tableDetails.getKeyDetails().getColumnCount() ); + identifierMapping.breakDownJdbcValues( + entityKey.getIdentifierValue(), + (valueIndex, value, jdbcValueMapping) -> { + final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() ); + valueParams.add( jdbcParameter ); + jdbcParameterBindings.addBinding( + jdbcParameter, + new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value ) + ); + }, + session + ); + final SqlTuple valueTuple = new SqlTuple( valueParams, identifierMapping ); + restriction.addExpression( valueTuple ); + } ); + } + + public void performActions(Map entityDetailsMap, QueryOptions lockingQueryOptions, SharedSessionContractImplementor session) { + final SessionFactoryImplementor sessionFactory = session.getSessionFactory(); + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + + final SelectStatement selectStatement = new SelectStatement( querySpec, domainResults ); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcServices.getDialect().getSqlAstTranslatorFactory(); + final SqlAstTranslator translator = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory, selectStatement ); + final JdbcSelect jdbcOperation = translator.translate( jdbcParameterBindings, lockingQueryOptions ); + + // IMPORTANT: we need a "clean" ExecutionContext to not further apply locking + final ExecutionContext executionContext = new BaseExecutionContext( session ); + final JdbcSelectExecutor jdbcSelectExecutor = jdbcServices.getJdbcSelectExecutor(); + final List results = jdbcSelectExecutor.executeQuery( + jdbcOperation, + jdbcParameterBindings, + executionContext, + row -> row, + Object[].class, + StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ), + ListResultsConsumer.instance( ListResultsConsumer.UniqueSemantic.ALLOW ) + ); + + if ( isEmpty( results ) ) { + throw new AssertionFailure( "Expecting results" ); + } + + results.forEach( (row) -> { + final Object id = row[0]; + final EntityDetails entityDetails = entityDetailsMap.get( id ); + for ( int i = 0; i < resultHandlers.size(); i++ ) { + // offset 1 because of the id at position 0 + resultHandlers.get( i ).applyResult( row[i+1], entityDetails, session ); + } + } ); + } + + + private interface ResultHandler { + void applyResult(Object state, EntityDetails entityDetails, SharedSessionContractImplementor session); + } + + private static abstract class AbstractResultHandler implements ResultHandler { + protected final Integer statePosition; + + public AbstractResultHandler(Integer statePosition) { + this.statePosition = statePosition; + } + } + + private static class NonToOneResultHandler extends AbstractResultHandler { + public NonToOneResultHandler(Integer statePosition) { + super( statePosition ); + } + + @Override + public void applyResult(Object stateValue, EntityDetails entityDetails, SharedSessionContractImplementor session) { + applyLoadedState( entityDetails, statePosition, stateValue ); + applyModelState( entityDetails, statePosition, stateValue ); + } + } + + private static class ToOneResultHandler extends AbstractResultHandler { + private final ToOneAttributeMapping toOne; + + public ToOneResultHandler(Integer statePosition, ToOneAttributeMapping toOne) { + super( statePosition ); + this.toOne = toOne; + } + + @Override + public void applyResult(Object stateValue, EntityDetails entityDetails, SharedSessionContractImplementor session) { + final Object reference; + if ( stateValue == null ) { + if ( !toOne.isNullable() ) { + throw new IllegalStateException( "Retrieved key was null, but to-one is not nullable : " + toOne.getNavigableRole().getFullPath() ); + } + else { + reference = null; + } + } + else { + reference = session.internalLoad( + toOne.getAssociatedEntityMappingType().getEntityName(), + stateValue, + false, + toOne.isNullable() + ); + } + applyLoadedState( entityDetails, statePosition, reference ); + applyModelState( entityDetails, statePosition, reference ); + } + } + + private static void applyLoadedState(EntityDetails entityDetails, Integer statePosition, Object stateValue) { + if ( entityDetails.entry().getLoadedState() != null ) { + entityDetails.entry().getLoadedState()[statePosition] = stateValue; + } + else { + if ( !entityDetails.entry().isReadOnly() ) { + throw new AssertionFailure( "Expecting entity entry to be read-only - " + entityDetails.instance() ); + } + } + } + + private static void applyModelState(EntityDetails entityDetails, Integer statePosition, Object reference) { + entityDetails.key().getPersister().getAttributeMapping( statePosition ).setValue( entityDetails.instance(), reference ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/package-info.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/package-info.java new file mode 100644 index 000000000000..5e36fad0a573 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ + +/** + * Support for follow-on locking. + * + * @author Steve Ebersole + */ +package org.hibernate.sql.exec.internal.lock; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/CacheableJdbcOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/CacheableJdbcOperation.java new file mode 100644 index 000000000000..c91683771dc8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/CacheableJdbcOperation.java @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.hibernate.query.spi.QueryOptions; + +/** + * Optional contract for {@linkplain JdbcOperation} implementors allowing them + * to be used with Query caching. + * + * @author Steve Ebersole + */ +public interface CacheableJdbcOperation { + /** + * Signals that the SQL depends on the parameter bindings - e.g., due to the need for inlining + * of parameter values or multiValued parameters. + */ + boolean dependsOnParameterBindings(); + + /** + * Whether the given arguments are compatible with this operation's state. Or, + * conversely, whether the arguments preclude this operation from being a cache-hit. + */ + boolean isCompatibleWith(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcLockStrategy.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcLockStrategy.java index 23e10534c05d..8f4224b51a78 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcLockStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcLockStrategy.java @@ -4,6 +4,8 @@ */ package org.hibernate.sql.exec.spi; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; + /** * The strategy to use for applying locks to a {@link JdbcOperationQuerySelect}. * diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcMutation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcMutation.java new file mode 100644 index 000000000000..1334d9bfb5d2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcMutation.java @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +/** + * Primary operation, which is an ({@code INSERT}, {@code UPDATE} or {@code DELETE}) performed via JDBC. + * + * @author Steve Ebersole + */ +public interface JdbcMutation extends PrimaryOperation { +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperation.java index bf650baa9a04..18a3c515ad77 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperation.java @@ -5,23 +5,28 @@ package org.hibernate.sql.exec.spi; import java.util.List; +import java.util.Set; /** * A JDBC operation to perform. This always equates to * some form of JDBC {@link java.sql.PreparedStatement} or - * {@link java.sql.CallableStatement} execution + * {@link java.sql.CallableStatement} execution. * * @author Steve Ebersole */ public interface JdbcOperation { /** - * Get the SQL command we will be executing through JDBC PreparedStatement - * or CallableStatement + * The SQL command we will be executing through JDBC. */ String getSqlString(); /** - * Get the list of parameter binders for the generated PreparedStatement + * The names of tables referred to by this operation. + */ + Set getAffectedTableNames(); + + /** + * The list of parameter binders for the generated PreparedStatement. */ List getParameterBinders(); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuery.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuery.java index 01643c88a935..0f80c3042016 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQuery.java @@ -4,30 +4,17 @@ */ package org.hibernate.sql.exec.spi; -import java.util.Map; -import java.util.Set; - -import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.Query; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import java.util.Map; + /** - * Unifying contract for any SQL statement we want to execute via JDBC. + * A JdbcOperation which (possibly) originated from a {@linkplain Query}. * * @author Steve Ebersole */ -public interface JdbcOperationQuery extends JdbcOperation { - - /** - * The names of tables this operation refers to - */ - Set getAffectedTableNames(); - - /** - * Signals that the SQL depends on the parameter bindings e.g. due to the need for inlining - * of parameter values or multiValued parameters. - */ - boolean dependsOnParameterBindings(); - +public interface JdbcOperationQuery extends JdbcOperation, CacheableJdbcOperation { /** * The parameters which were inlined into the query as literals. * @@ -35,6 +22,4 @@ public interface JdbcOperationQuery extends JdbcOperation { */ @Deprecated(since = "7.0", forRemoval = true) Map getAppliedParameters(); - - boolean isCompatibleWith(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutation.java index 32499d4aef0f..d35ffe800902 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryMutation.java @@ -16,6 +16,6 @@ * * @author Steve Ebersole */ -public interface JdbcOperationQueryMutation extends JdbcOperationQuery { +public interface JdbcOperationQueryMutation extends JdbcOperationQuery, JdbcMutation { } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelect.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelect.java new file mode 100644 index 000000000000..9fb1f4d08631 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelect.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Incubating; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; + +import java.sql.Connection; + +/** + * Primary operation which is a {@code SELECT} performed via JDBC. + * + * @author Steve Ebersole + */ +@Incubating +public interface JdbcSelect extends PrimaryOperation, CacheableJdbcOperation { + JdbcValuesMappingProducer getJdbcValuesMappingProducer(); + JdbcLockStrategy getLockStrategy(); + boolean usesLimitParameters(); + JdbcParameter getLimitParameter(); + int getRowsToSkip(); + int getMaxRows(); + + /** + * Access to a collector of values loaded to be applied during the + * processing of the selection's results. + * May be {@code null}. + */ + @Nullable + LoadedValuesCollector getLoadedValuesCollector(); + + /** + * Perform any pre-actions. + *

+ * Generally the pre-actions should use the passed {@code jdbcStatementAccess} to interact with the + * database, although the {@code jdbcConnection} can be used to create specialized statements, + * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc. + * + * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action. + * @param jdbcConnection The JDBC Connection. + * @param executionContext Access to contextual information useful while executing. + */ + void performPreActions(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext); /** + + * Perform any post-actions. + *

+ * Generally the post-actions should use the passed {@code jdbcStatementAccess} to interact with the + * database, although the {@code jdbcConnection} can be used to create specialized statements, + * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc. + * + * @param succeeded Whether the primary operation succeeded. + * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action. + * @param jdbcConnection The JDBC Connection. + * @param executionContext Access to contextual information useful while executing. + */ + void performPostAction( + boolean succeeded, + StatementAccess jdbcStatementAccess, + Connection jdbcConnection, + ExecutionContext executionContext); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java index 0278017ba46d..1372aa0c59ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcSelectExecutor.java @@ -4,11 +4,8 @@ */ package org.hibernate.sql.exec.spi; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.List; -import java.util.Set; - +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; import org.hibernate.FlushMode; import org.hibernate.Incubating; import org.hibernate.LockOptions; @@ -27,8 +24,10 @@ import org.hibernate.sql.results.spi.RowTransformer; import org.hibernate.sql.results.spi.ScrollableResultsConsumer; -import jakarta.persistence.CacheRetrieveMode; -import jakarta.persistence.CacheStoreMode; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Set; /** * An executor for JdbcSelect operations. @@ -42,7 +41,7 @@ public interface JdbcSelectExecutor { * @since 6.6 */ T executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -54,7 +53,7 @@ T executeQuery( * @since 6.6 */ default T executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -77,7 +76,7 @@ default T executeQuery( * @since 6.6 */ default T executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -97,7 +96,7 @@ default T executeQuery( } default List list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -106,7 +105,7 @@ default List list( } default List list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -127,7 +126,7 @@ default List list( * @since 6.6 */ default List list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -147,7 +146,7 @@ default List list( } default ScrollableResultsImplementor scroll( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, ScrollMode scrollMode, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, @@ -159,7 +158,7 @@ default ScrollableResultsImplementor scroll( * @since 6.6 */ default ScrollableResultsImplementor scroll( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, ScrollMode scrollMode, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, 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 new file mode 100644 index 000000000000..054515b97b98 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/LoadedValuesCollector.java @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.spi.NavigablePath; + +import java.util.List; + +/** + * Used to collect entity and collection values which are loaded as part of + * {@linkplain org.hibernate.sql.results.jdbc.spi.JdbcValues} processing. + * Kept as part of {@linkplain org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState} + * + * @author Steve Ebersole + */ +public interface LoadedValuesCollector { + /** + * Register a loading entity. + * + * @param navigablePath The NavigablePath relative to the SQL AST used to load the entity + * @param entityDescriptor The descriptor for the entity being loaded. + * @param entityKey The EntityKey for the entity being loaded + */ + void registerEntity( + NavigablePath navigablePath, + EntityMappingType entityDescriptor, + EntityKey entityKey); + + /** + * Register a loading collection. + * + * @param navigablePath The NavigablePath relative to the SQL AST used to load the entity + * @param collectionDescriptor The descriptor for the collection being loaded. + * @param collectionKey The CollectionKey for the collection being loaded + */ + void registerCollection( + NavigablePath navigablePath, + PluralAttributeMapping collectionDescriptor, + CollectionKey collectionKey); + + /** + * Clears the state of the collector. + * + * @implSpec In some cases, the collector may be cached as part of a + * JdbcSelect being cached (see {@linkplain JdbcSelect#getLoadedValuesCollector()}. + * This method allows clearing of the internal state after execution of the JdbcSelect. + */ + void clear(); + + /** + * Access to all root entities loaded. + */ + List getCollectedRootEntities(); + + /** + * Access to all non-root entities (join fetches e.g.) loaded. + */ + List getCollectedNonRootEntities(); + + /** + * Access to all collection loaded. + */ + List getCollectedCollections(); + + interface LoadedPartRegistration { + NavigablePath navigablePath(); + ModelPart modelPart(); + } + + /** + * Details about a loaded entity. + */ + record LoadedEntityRegistration( + NavigablePath navigablePath, + EntityMappingType entityDescriptor, + EntityKey entityKey) implements LoadedPartRegistration { + @Override + public EntityMappingType modelPart() { + return entityDescriptor(); + } + } + + /** + * Details about a loaded collection. + */ + record LoadedCollectionRegistration( + NavigablePath navigablePath, + PluralAttributeMapping collectionDescriptor, + CollectionKey collectionKey) implements LoadedPartRegistration { + @Override + public PluralAttributeMapping modelPart() { + return collectionDescriptor(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java new file mode 100644 index 000000000000..ff08def0d740 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.hibernate.Incubating; + +import java.sql.Connection; + +/** + * An action to be performed after a {@linkplain PrimaryOperation}. + */ +@Incubating +@FunctionalInterface +public interface PostAction extends SecondaryAction { + /** + * Perform the action. + *

+ * Generally the action should use the passed {@code jdbcStatementAccess} to interact with the + * database, although the {@code jdbcConnection} can be used to create specialized statements, + * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc. + * + * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action. + * @param jdbcConnection The JDBC Connection. + * @param executionContext Access to contextual information useful while executing. + */ + void performPostAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext); + + /** + * Should this post-action always be run even if the primary operation fails? + */ + default boolean shouldRunAfterFail() { + return false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java new file mode 100644 index 000000000000..f8fdb063b6b8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.hibernate.Incubating; + +import java.sql.Connection; + +/** + * An action to be performed before a {@linkplain PrimaryOperation}. + */ +@Incubating +@FunctionalInterface +public interface PreAction extends SecondaryAction { + /** + * Perform the action. + *

+ * Generally the action should use the passed {@code jdbcStatementAccess} to interact with the + * database, although the {@code jdbcConnection} can be used to create specialized statements, + * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc. + * + * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action. + * @param jdbcConnection The JDBC Connection. + * @param executionContext Access to contextual information useful while executing. + */ + void performPreAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PrimaryOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PrimaryOperation.java new file mode 100644 index 000000000000..dc0adfb4ba6d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PrimaryOperation.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import java.util.Set; + +/** + * A primary operation to be executed using JDBC. + * + * @see org.hibernate.sql.exec.internal.JdbcSelectWithActions + * + * @author Steve Ebersole + */ +public interface PrimaryOperation extends JdbcOperation { + /** + * The names of tables this operation refers to + */ + Set getAffectedTableNames(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java new file mode 100644 index 000000000000..a83abe85bbac --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.hibernate.Incubating; + +/** + * Common marker interface for {@linkplain PreAction} and {@linkplain PostAction}. + * + * @implSpec Split to allow implementing both simultaneously. + * + * @author Steve Ebersole + */ +@Incubating +public interface SecondaryAction { +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StandardEntityInstanceResolver.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StandardEntityInstanceResolver.java deleted file mode 100644 index 922f0b77c360..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StandardEntityInstanceResolver.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.sql.exec.spi; - -import org.hibernate.engine.spi.EntityHolder; -import org.hibernate.engine.spi.EntityKey; -import org.hibernate.engine.spi.SharedSessionContractImplementor; - -/** - * @author Steve Ebersole - */ -public class StandardEntityInstanceResolver { - private StandardEntityInstanceResolver() { - } - - public static Object resolveEntityInstance( - EntityKey entityKey, - boolean eager, - SharedSessionContractImplementor session) { - final EntityHolder holder = session.getPersistenceContext().getEntityHolder( entityKey ); - if ( holder != null && holder.isEventuallyInitialized() ) { - return holder.getEntity(); - } - - // Lastly, try to load from database - return session.internalLoad( - entityKey.getEntityName(), - entityKey.getIdentifier(), - eager, - false - ); - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java new file mode 100644 index 000000000000..3891fa1a878e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import java.sql.Statement; + +/** + * Access to a JDBC {@linkplain Statement}. + * + * @apiNote Intended for cases where sharing a common JDBC {@linkplain Statement} is useful, generally for performance. + * @implNote Manages various tasks around creation and ensuring it gets cleaned up. + * + * @author Steve Ebersole + */ +public interface StatementAccess { + /** + * Access the JDBC {@linkplain Statement}. + */ + Statement getJdbcStatement(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementOptions.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementOptions.java deleted file mode 100644 index 38bd6e54175c..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementOptions.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.sql.exec.spi; - -/** - * Options for the creation of a JDBC statement - * - * @author Steve Ebersole - */ -public class StatementOptions { - public static final StatementOptions NONE = new StatementOptions( -1, -1, -1, -1 ); - - private final Integer firstRow; - private final Integer maxRows; - private final Integer timeoutInMilliseconds; - private final Integer fetchSize; - - public StatementOptions( - Integer firstRow, - Integer maxRows, - Integer timeoutInMilliseconds, - Integer fetchSize) { - this.firstRow = firstRow; - this.maxRows = maxRows; - this.timeoutInMilliseconds = timeoutInMilliseconds; - this.fetchSize = fetchSize; - } - - public boolean hasLimit() { - return ( firstRow != null && firstRow > 0 ) - || ( maxRows != null && maxRows > 0 ); - } - - public Integer getFirstRow() { - return firstRow; - } - - public Integer getMaxRows() { - return maxRows; - } - - public boolean hasTimeout() { - return timeoutInMilliseconds != null && timeoutInMilliseconds > 0; - } - - public Integer getTimeoutInMilliseconds() { - return timeoutInMilliseconds; - } - - public boolean hasFetchSize() { - return fetchSize != null && fetchSize > 0; - } - - public Integer getFetchSize() { - return fetchSize; - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/package-info.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/package-info.java index a4fa62d6b878..c8e9f8d1f67f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/package-info.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/package-info.java @@ -5,12 +5,21 @@ /** * SPI for execution of SQL statements via JDBC. The statement to execute is - * modelled by {@link org.hibernate.sql.exec.spi.JdbcOperationQuery} and is - * executed via the corresponding executor. - *

+ * modeled by {@link org.hibernate.sql.exec.spi.JdbcOperation} and is + * executed via the corresponding executor - + * either {@linkplain org.hibernate.sql.exec.spi.JdbcSelectExecutor} + * or {@linkplain org.hibernate.sql.exec.spi.JdbcMutationExecutor}. + *

* For operations that return {@link java.sql.ResultSet}s, be sure to see * {@link org.hibernate.sql.results} which provides support for processing results - * starting with {@link org.hibernate.sql.results.jdbc.spi.JdbcValuesMapping} + * starting with {@link org.hibernate.sql.results.jdbc.spi.JdbcValuesMapping}. + *

+ * Also provides support for pessimistic locking as part of + * {@linkplain org.hibernate.sql.exec.spi.JdbcSelect JDBC select} handling. For details, + * see {@linkplain org.hibernate.sql.exec.internal.JdbcSelectWithActions}, + * {@linkplain org.hibernate.sql.exec.spi.JdbcSelect#getLoadedValuesCollector()}, + * {@linkplain org.hibernate.sql.exec.internal.lock.FollowOnLockingAction} + * and friends. */ @Incubating package org.hibernate.sql.exec.spi; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/ColumnValuesTableMutationBuilder.java b/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/ColumnValuesTableMutationBuilder.java index aa875efce2d5..b7344ed16be6 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/ColumnValuesTableMutationBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/ColumnValuesTableMutationBuilder.java @@ -59,7 +59,7 @@ default void addValueColumn(SelectableMapping selectableMapping) { /** * Add a key column */ - default void addKeyColumn(SelectableMapping selectableMapping) { + default void addKeyColumn(int index, SelectableMapping selectableMapping) { addKeyColumn( selectableMapping.getSelectionExpression(), selectableMapping.getWriteExpression(), diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/AbstractJdbcMutation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/AbstractJdbcMutation.java index ae3806e03011..d32499fcb939 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/AbstractJdbcMutation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/AbstractJdbcMutation.java @@ -5,6 +5,7 @@ package org.hibernate.sql.model.jdbc; import java.util.List; +import java.util.Set; import org.hibernate.engine.jdbc.mutation.ParameterUsage; import org.hibernate.engine.jdbc.mutation.internal.JdbcValueDescriptorImpl; @@ -57,6 +58,11 @@ public TableMapping getTableDetails() { return tableDetails; } + @Override + public Set getAffectedTableNames() { + return Set.of( getTableDetails().getTableName() ); + } + @Override public MutationTarget getMutationTarget() { return mutationTarget; 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 97b0d9691b61..fffe229e72aa 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 @@ -17,7 +17,6 @@ import org.hibernate.sql.results.graph.InitializerData; import org.hibernate.sql.results.graph.entity.EntityFetch; import org.hibernate.sql.results.jdbc.internal.JdbcValuesCacheHit; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; import org.hibernate.sql.results.jdbc.spi.RowProcessingState; @@ -28,7 +27,7 @@ */ public class RowProcessingStateStandardImpl extends BaseExecutionContext implements RowProcessingState { - private final JdbcValuesSourceProcessingStateStandardImpl resultSetProcessingState; + private final JdbcValuesSourceProcessingState resultSetProcessingState; private final RowReader rowReader; private final JdbcValues jdbcValues; @@ -38,7 +37,7 @@ public class RowProcessingStateStandardImpl extends BaseExecutionContext impleme private final InitializerData[] initializerData; public RowProcessingStateStandardImpl( - JdbcValuesSourceProcessingStateStandardImpl resultSetProcessingState, + JdbcValuesSourceProcessingState resultSetProcessingState, ExecutionContext executionContext, RowReader rowReader, JdbcValues jdbcValues) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java index 7a7ad7feaef6..fde5c29cf3ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java @@ -4,35 +4,35 @@ */ package org.hibernate.sql.results.jdbc.internal; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.Locking; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.NoopLimitHandler; -import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.JdbcCoordinator; +import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionEventListenerManager; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.event.monitor.spi.EventMonitor; import org.hibernate.event.monitor.spi.DiagnosticEvent; +import org.hibernate.event.monitor.spi.EventMonitor; import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; import org.hibernate.resource.jdbc.spi.JdbcSessionContext; import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; +import org.hibernate.sql.exec.internal.lock.FollowOnLockingAction; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcLockStrategy; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + import static java.util.Collections.emptyMap; import static org.hibernate.engine.jdbc.JdbcLogging.JDBC_LOGGER; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; @@ -42,7 +42,7 @@ */ public class DeferredResultSetAccess extends AbstractResultSetAccess { - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; private final JdbcParameterBindings jdbcParameterBindings; private final ExecutionContext executionContext; private final JdbcSelectExecutor.StatementCreator statementCreator; @@ -50,14 +50,13 @@ public class DeferredResultSetAccess extends AbstractResultSetAccess { private final String finalSql; private final Limit limit; private final LimitHandler limitHandler; - private final boolean usesFollowOnLocking; private final int resultCountEstimate; private PreparedStatement preparedStatement; private ResultSet resultSet; public DeferredResultSetAccess( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, JdbcSelectExecutor.StatementCreator statementCreator, @@ -77,7 +76,6 @@ public DeferredResultSetAccess( finalSql = jdbcSelect.getSqlString(); limit = null; limitHandler = NoopLimitHandler.NO_LIMIT; - usesFollowOnLocking = false; } else { // Note that limit and lock aren't set for SQM as that is applied during SQL rendering @@ -96,13 +94,12 @@ public DeferredResultSetAccess( queryOptions ); - final LockOptions lockOptions = queryOptions.getLockOptions(); - final JdbcLockStrategy jdbcLockStrategy = jdbcSelect.getLockStrategy(); + final var lockOptions = queryOptions.getLockOptions(); + final var jdbcLockStrategy = jdbcSelect.getLockStrategy(); final String sqlWithLocking; if ( hasLocking( jdbcLockStrategy, lockOptions ) ) { - usesFollowOnLocking = useFollowOnLocking( jdbcLockStrategy, sqlWithLimit, queryOptions, lockOptions, dialect ); + final boolean usesFollowOnLocking = useFollowOnLocking( jdbcLockStrategy, sqlWithLimit, queryOptions, lockOptions, dialect ); if ( usesFollowOnLocking ) { - handleFollowOnLocking( executionContext, lockOptions ); sqlWithLocking = sqlWithLimit; } else { @@ -110,18 +107,18 @@ public DeferredResultSetAccess( } } else { - usesFollowOnLocking = false; sqlWithLocking = sqlWithLimit; } - final boolean commentsEnabled = - executionContext.getSession().getFactory() - .getSessionFactoryOptions().isCommentsEnabled(); + final boolean commentsEnabled = executionContext.getSession() + .getFactory() + .getSessionFactoryOptions() + .isCommentsEnabled(); finalSql = dialect.addSqlHintOrComment( sqlWithLocking, queryOptions, commentsEnabled ); } } - private boolean needsLimitHandler(JdbcOperationQuerySelect jdbcSelect) { + private boolean needsLimitHandler(JdbcSelect jdbcSelect) { return limit != null && !limit.isEmpty() && !jdbcSelect.usesLimitParameters(); } @@ -129,38 +126,11 @@ private static boolean hasLocking(JdbcLockStrategy jdbcLockStrategy, LockOptions return jdbcLockStrategy != JdbcLockStrategy.NONE && lockOptions != null && !lockOptions.isEmpty(); } - private void handleFollowOnLocking(ExecutionContext executionContext, LockOptions lockOptions) { - final LockMode lockMode = determineFollowOnLockMode( lockOptions ); - if ( lockMode != LockMode.UPGRADE_SKIPLOCKED ) { - if ( lockOptions.getLockMode() != LockMode.NONE ) { - CORE_LOGGER.usingFollowOnLocking(); - } - - final LockOptions lockOptionsToUse = new LockOptions( - lockMode, - lockOptions.getTimeOut(), - lockOptions.getScope(), - Locking.FollowOn.ALLOW - ); - - registerAfterLoadAction( executionContext, lockOptionsToUse ); - } - } - - /** - * For Hibernate Reactive - */ - protected void registerAfterLoadAction(ExecutionContext executionContext, LockOptions lockOptionsToUse) { - executionContext.getCallback() - .registerAfterLoadAction( (entity, persister, session) -> - session.lock( persister.getEntityName(), entity, lockOptionsToUse ) ); - } - private static boolean useFollowOnLocking( JdbcLockStrategy jdbcLockStrategy, String sql, QueryOptions queryOptions, - LockOptions lockOptions, + @SuppressWarnings("removal") LockOptions lockOptions, Dialect dialect) { assert lockOptions != null; return switch ( jdbcLockStrategy ) { @@ -173,8 +143,9 @@ private static boolean useFollowOnLocking( private static boolean interpretAutoLockStrategy( String sql, QueryOptions queryOptions, - LockOptions lockOptions, + @SuppressWarnings("removal") LockOptions lockOptions, Dialect dialect) { + //noinspection removal return switch ( lockOptions.getFollowOnStrategy() ) { case ALLOW -> dialect.useFollowOnLocking( sql, queryOptions ); case FORCE -> true; @@ -182,6 +153,20 @@ private static boolean interpretAutoLockStrategy( }; } + + /** + * For Hibernate Reactive + * + * @deprecated Was only used for follow-on locking, which is now handled differently; + * see {@linkplain FollowOnLockingAction} + */ + @Deprecated(since = "7.2", forRemoval = true) + protected void registerAfterLoadAction(ExecutionContext executionContext, LockOptions lockOptionsToUse) { + executionContext.getCallback() + .registerAfterLoadAction( (entity, persister, session) -> + session.lock( persister.getEntityName(), entity, lockOptionsToUse ) ); + } + public LimitHandler getLimitHandler() { return limitHandler; } @@ -207,10 +192,6 @@ public String getFinalSql() { return finalSql; } - public boolean usesFollowOnLocking() { - return usesFollowOnLocking; - } - protected void bindParameters(PreparedStatement preparedStatement) throws SQLException { setQueryOptions( preparedStatement ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesSourceProcessingStateStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesSourceProcessingStateStandardImpl.java index 09f722565e38..a45c3200e46f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesSourceProcessingStateStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesSourceProcessingStateStandardImpl.java @@ -17,6 +17,7 @@ import org.hibernate.event.spi.PreLoadEvent; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; import org.hibernate.sql.results.graph.collection.LoadingCollectionEntry; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; @@ -25,9 +26,9 @@ * @author Steve Ebersole */ public class JdbcValuesSourceProcessingStateStandardImpl implements JdbcValuesSourceProcessingState { - - private final ExecutionContext executionContext; private final JdbcValuesSourceProcessingOptions processingOptions; + private final LoadedValuesCollector loadedValuesCollector; + private final ExecutionContext executionContext; private List loadingEntityHolders; private List reloadedEntityHolders; @@ -37,8 +38,10 @@ public class JdbcValuesSourceProcessingStateStandardImpl implements JdbcValuesSo private final PostLoadEvent postLoadEvent; public JdbcValuesSourceProcessingStateStandardImpl( - ExecutionContext executionContext, - JdbcValuesSourceProcessingOptions processingOptions) { + LoadedValuesCollector loadedValuesCollector, + JdbcValuesSourceProcessingOptions processingOptions, + ExecutionContext executionContext) { + this.loadedValuesCollector = loadedValuesCollector; this.executionContext = executionContext; this.processingOptions = processingOptions; @@ -53,6 +56,12 @@ public JdbcValuesSourceProcessingStateStandardImpl( } } + public JdbcValuesSourceProcessingStateStandardImpl( + ExecutionContext executionContext, + JdbcValuesSourceProcessingOptions processingOptions) { + this( null, processingOptions, executionContext ); + } + @Override public ExecutionContext getExecutionContext() { return executionContext; @@ -84,6 +93,14 @@ public void registerLoadingEntityHolder(EntityHolder holder) { loadingEntityHolders = new ArrayList<>(); } loadingEntityHolders.add( holder ); + + if ( loadedValuesCollector != null ) { + loadedValuesCollector.registerEntity( + holder.getEntityInitializer().getNavigablePath(), + holder.getDescriptor(), + holder.getEntityKey() + ); + } } @Override @@ -114,8 +131,15 @@ public void registerLoadingCollection(CollectionKey key, LoadingCollectionEntry if ( loadingCollectionMap == null ) { loadingCollectionMap = new HashMap<>(); } - loadingCollectionMap.put( key, loadingCollectionEntry ); + + if ( loadedValuesCollector != null ) { + loadedValuesCollector.registerCollection( + loadingCollectionEntry.getInitializer().getNavigablePath(), + loadingCollectionEntry.getCollectionDescriptor().getAttributeMapping(), + key + ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java index 76381a11ac27..1698411f91c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java @@ -15,9 +15,9 @@ import org.hibernate.query.ResultListTransformer; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.EntityJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; @@ -144,7 +144,7 @@ public List consume( JdbcValues jdbcValues, SharedSessionContractImplementor session, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader) { rowReader.startLoading( rowProcessingState ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ManagedResultConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ManagedResultConsumer.java index 10a6a5613b49..5dec8e2fec6a 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ManagedResultConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ManagedResultConsumer.java @@ -8,9 +8,9 @@ import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; /** * Reads rows without producing a result. @@ -27,7 +27,7 @@ public Void consume( JdbcValues jdbcValues, SharedSessionContractImplementor session, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader) { final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ResultsConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ResultsConsumer.java index cdb09ab295df..ccc12400b822 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ResultsConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ResultsConsumer.java @@ -6,9 +6,9 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; /** * Consumes {@link JdbcValues} and returns the consumed values in whatever form this @@ -21,7 +21,7 @@ T consume( JdbcValues jdbcValues, SharedSessionContractImplementor session, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ScrollableResultsConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ScrollableResultsConsumer.java index 540780ec77e8..52bf6d4251e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ScrollableResultsConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ScrollableResultsConsumer.java @@ -15,8 +15,8 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.query.spi.ScrollableResultsImplementor; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; /** * @author Steve Ebersole @@ -41,7 +41,7 @@ public ScrollableResultsImplementor consume( JdbcValues jdbcValues, SharedSessionContractImplementor session, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader) { rowReader.startLoading( rowProcessingState ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java index d0b09f1e0d98..398c91cd1141 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java @@ -9,9 +9,9 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.query.SelectionQuery; import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; -import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValues; import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState; /** * Used beneath {@link SelectionQuery#getResultCount()}. @@ -35,7 +35,7 @@ public T consume( JdbcValues jdbcValues, SharedSessionContractImplementor session, JdbcValuesSourceProcessingOptions processingOptions, - JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState, RowProcessingStateStandardImpl rowProcessingState, RowReader rowReader) { final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cfg/persister/GoofyPersisterClassProvider.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cfg/persister/GoofyPersisterClassProvider.java index 4fc818bb05a2..99f27fb5d6eb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/cfg/persister/GoofyPersisterClassProvider.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cfg/persister/GoofyPersisterClassProvider.java @@ -164,6 +164,11 @@ public TableDetails getIdentifierTableDetails() { throw new UnsupportedOperationException(); } + @Override + public void forEachTableDetails(Consumer consumer) { + throw new UnsupportedOperationException(); + } + @Override public ModelPart findSubPart( String name, EntityMappingType targetType) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/OracleFollowOnLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/OracleFollowOnLockingTest.java index 9be20c617a28..cf44a8c029f8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/OracleFollowOnLockingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/OracleFollowOnLockingTest.java @@ -4,23 +4,7 @@ */ package org.hibernate.orm.test.dialect.functional; -import java.util.List; - -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.hibernate.Session; -import org.hibernate.boot.SessionFactoryBuilder; -import org.hibernate.dialect.OracleDialect; -import org.hibernate.jpa.AvailableHints; -import org.hibernate.query.IllegalQueryOperationException; - -import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.JiraKey; -import org.hibernate.testing.jdbc.SQLStatementInterceptor; -import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; -import org.junit.Before; -import org.junit.Test; - +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -30,535 +14,517 @@ import jakarta.persistence.LockModeType; import jakarta.persistence.ManyToOne; import jakarta.persistence.NamedQuery; +import jakarta.persistence.OneToMany; import jakarta.persistence.QueryHint; +import jakarta.persistence.Table; +import org.hibernate.Locking; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.hibernate.LockMode.PESSIMISTIC_WRITE; +import static org.junit.jupiter.api.Assertions.fail; /** * @author Vlad Mihalcea */ +@SuppressWarnings("JUnitMalformedDeclaration") @RequiresDialect(OracleDialect.class) @JiraKey(value = "HHH-9486") -public class OracleFollowOnLockingTest extends - BaseNonConfigCoreFunctionalTestCase { - - private SQLStatementInterceptor sqlStatementInterceptor; +@DomainModel(annotatedClasses = { + OracleFollowOnLockingTest.Product.class, + OracleFollowOnLockingTest.Vehicle.class, + OracleFollowOnLockingTest.SportsCar.class, + OracleFollowOnLockingTest.Truck.class, + OracleFollowOnLockingTest.Customer.class, + OracleFollowOnLockingTest.Purchase.class +}) +@SessionFactory(useCollectingStatementInspector = true) +public class OracleFollowOnLockingTest { + @BeforeEach + void createTestData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + for ( int i = 0; i < 50; i++ ) { + Product product = new Product(); + product.name = "Product " + i % 10; + session.persist( product ); + } + + Truck truck1 = new Truck(); + Truck truck2 = new Truck(); + SportsCar sportsCar1 = new SportsCar(); + session.persist( truck1 ); + session.persist( truck2 ); + session.persist( sportsCar1 ); + + final Customer billyBob = new Customer( 1, "Billy Bob" ); + final Purchase purchase1 = new Purchase( 1, Instant.now(), 123.00, billyBob ); + final Purchase purchase2 = new Purchase( 2, Instant.now(), 789.00, billyBob ); + session.persist( billyBob ); + session.persist( purchase1 ); + session.persist( purchase2 ); + } ); - @Override - protected void configureSessionFactoryBuilder(SessionFactoryBuilder sfb) { - sqlStatementInterceptor = new SQLStatementInterceptor( sfb ); } - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Product.class, - Vehicle.class, - SportsCar.class, - Truck.class - }; + @AfterEach + void dropTestData(SessionFactoryScope factoryScope) { + factoryScope.dropData(); } - @Before - public void init() { - final Session session = openSession(); - session.beginTransaction(); - - for ( int i = 0; i < 50; i++ ) { - Product product = new Product(); - product.name = "Product " + i % 10; - session.persist( product ); - } + @Test + void testLockAcrossJoin(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - Truck truck1 = new Truck(); - Truck truck2 = new Truck(); - SportsCar sportsCar1 = new SportsCar(); - session.persist( truck1 ); - session.persist( truck2 ); - session.persist( sportsCar1 ); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - session.getTransaction().commit(); - session.close(); - } + final List customers = session.createSelectionQuery( + "select c from Customer c outer join fetch c.purchases", Customer.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.ROOT_ONLY ) + .getResultList(); - @Override - protected boolean isCleanupTestDataRequired() { - return true; + assertThat( customers ).hasSize( 1 ); + // See the note on `OracleLockingSupport#getOuterJoinLockingType`. + // As of 23 at least Oracle does support locking across joins - I've verified this locally. + // Need CI to tell us about previous versions... + // there should be no follow-on locking (again as of 23 at least) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " for update of " ); + } ); } @Test - public void testPessimisticLockWithMaxResultsThenNoFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); + public void testPessimisticLockWithMaxResultsThenNoFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - sqlStatementInterceptor.getSqlQueries().clear(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - List products = - session.createQuery( - "select p from Product p", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .setMaxResults( 10 ) - .getResultList(); - - assertEquals( 10, products.size() ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select p from Product p", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + // there should be no follow-on locking - so just 1 + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithFirstResultThenFollowOnLocking() { + public void testPessimisticLockWithFirstResultThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - final Session session = openSession(); - session.beginTransaction(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - sqlStatementInterceptor.getSqlQueries().clear(); - - List products = - session.createQuery( - "select p from Product p", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .setFirstResult( 40 ) - .setMaxResults( 10 ) - .getResultList(); - - assertEquals( 10, products.size() ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select p from Product p", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFirstResult( 40 ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + // there should be no follow-on locking - so just 1 + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithFirstResultAndJoinThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); + public void testPessimisticLockWithFirstResultAndJoinThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - List products = - session.createQuery( - "select p from Product p left join p.vehicle v on v.id is null", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .setFirstResult( 40 ) - .setMaxResults( 10 ) - .getResultList(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - assertEquals( 10, products.size() ); - assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select p from Product p left join p.vehicle v on v.id is null", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFirstResult( 40 ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + // this should trigger follow-on locking - so 2 (initial query, lock) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).doesNotContain( " for update of " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).doesNotContain( " join " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " for update of " ); + } ); } @Test - public void testPessimisticLockWithNamedQueryExplicitlyEnablingFollowOnLockingThenFollowOnLocking() { + public void testPessimisticLockWithNamedQueryExplicitlyEnablingFollowOnLockingThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - final Session session = openSession(); - session.beginTransaction(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - sqlStatementInterceptor.getSqlQueries().clear(); - - List products = session.createNamedQuery( - "product_by_name", Product.class ) - .getResultList(); - - assertEquals( 50, products.size() ); - assertEquals( 51, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createNamedQuery( "product_by_name", Product.class ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 50 ); + // this should trigger follow-on locking - so 2 (initial query, lock) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } ); } @Test - public void testPessimisticLockWithCountDistinctThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); + public void testPessimisticLockWithCountDistinctThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - sqlStatementInterceptor.getSqlQueries().clear(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - List products = session.createQuery( - "select p from Product p where ( select count(distinct p1.id) from Product p1 ) > 0 ", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ).setFollowOnLocking( false ) ) - .getResultList(); - - assertEquals( 50, products.size() ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery("select p from Product p where ( select count(distinct p1.id) from Product p1 ) > 0 ", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 50 ); + // we disallow follow-on locking - so 1 (initial query) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithFirstResultWhileExplicitlyDisablingFollowOnLockingThenFails() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); + public void testPessimisticLockWithFirstResultWhileExplicitlyDisablingFollowOnLockingThenFails(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - List products = session.createQuery( "select p from Product p", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( false ) ) - .setFirstResult( 40 ) - .setMaxResults( 10 ) - .getResultList(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - assertEquals( 10, products.size() ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select p from Product p", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) + .setFirstResult( 40 ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + // we disallow follow-on locking - so 1 (initial query) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithFirstResultAndJoinWhileExplicitlyDisablingFollowOnLockingThenFails() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); - - try { - List products = - session.createQuery( - "select p from Product p left join p.vehicle v on v.id is null", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( false ) ) - .setFirstResult( 40 ) - .setMaxResults( 10 ) - .getResultList(); - fail( "Should throw exception since Oracle does not support ORDER BY if follow on locking is disabled" ); - } - catch ( IllegalStateException expected ) { - assertEquals( - IllegalQueryOperationException.class, - expected.getCause().getClass() - ); - assertThat( expected.getCause().getMessage() ) - .contains( "Locking with OFFSET/FETCH is not supported" ); - } + public void testPessimisticLockWithFirstResultAndJoinWhileExplicitlyDisablingFollowOnLockingThenFails(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + try { + List products = session.createQuery( "select p from Product p left join p.vehicle v on v.id is null", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) + .setFirstResult( 40 ) + .setMaxResults( 10 ) + .getResultList(); + fail( + "Should throw exception since Oracle does not support LIMIT if follow on locking is disabled" ); + } + catch ( IllegalStateException expected ) { + Assertions.assertEquals( IllegalQueryOperationException.class, expected.getCause().getClass() ); + assertThat( expected.getCause().getMessage() ) + .contains( "Locking with OFFSET/FETCH is not supported" ); + } + } ); } @Test - public void testPessimisticLockWithFirstResultsWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); + public void testPessimisticLockWithFirstResultsWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - List products = - session.createQuery( - "select p from Product p", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( true ) ) - .setFirstResult( 40 ) - .setMaxResults( 10 ) - .getResultList(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - assertEquals( 10, products.size() ); - assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select p from Product p", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.FORCE ) + .setFirstResult( 40 ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + // this should trigger follow-on locking - so 2 (initial query, lock) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } ); } @Test @JiraKey(value = "HHH-16433") - public void testPessimisticLockWithOrderByThenNoFollowOnLocking() { + public void testPessimisticLockWithOrderByThenNoFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - final Session session = openSession(); - session.beginTransaction(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - sqlStatementInterceptor.getSqlQueries().clear(); - - List products = - session.createQuery( - "select p from Product p order by p.id", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .getResultList(); - - assertTrue( products.size() > 1 ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select p from Product p order by p.id", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 50 ); + //assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + // this should not trigger follow-on locking apparently - so 1 (initial query) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithMaxResultsAndOrderByThenNoFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); - - List products = - session.createQuery( - "select p from Product p order by p.id", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .setMaxResults( 10 ) - .getResultList(); - - assertEquals( 10, products.size() ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + public void testPessimisticLockWithMaxResultsAndOrderByThenNoFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + List products = session.createQuery( "select p from Product p order by p.id", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + // this should not trigger follow-on locking + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithMaxResultsAndOrderByWhileExplicitlyDisablingFollowOnLocking() { + public void testPessimisticLockWithMaxResultsAndOrderByWhileExplicitlyDisablingFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - final Session session = openSession(); - session.beginTransaction(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - sqlStatementInterceptor.getSqlQueries().clear(); + List products = session.createQuery( "select p from Product p order by p.id", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) + .setMaxResults( 10 ) + .getResultList(); - List products = - session.createQuery( - "select p from Product p order by p.id", - Product.class - ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( false ) ) - .setMaxResults( 10 ) - .getResultList(); - assertEquals( 10, products.size() ); - assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + assertThat( products ).hasSize( 10 ); + //assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() ); + // this should not trigger follow-on locking apparently - so 1 (initial query) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } ); } @Test - public void testPessimisticLockWithMaxResultsAndOrderByWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking() { + public void testPessimisticLockWithMaxResultsAndOrderByWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - final Session session = openSession(); - session.beginTransaction(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - sqlStatementInterceptor.getSqlQueries().clear(); - - List products = - session.createQuery( - "select p from Product p order by p.id", Product.class ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( true ) ) - .setMaxResults( 10 ) - .getResultList(); + List products = session.createQuery( "select p from Product p order by p.id", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.FORCE ) + .setMaxResults( 10 ) + .getResultList(); - assertEquals( 10, products.size() ); - assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() ); + assertThat( products ).hasSize( 10 ); + // this should trigger follow-on locking - so 2 (initial query, locking) + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); - session.getTransaction().commit(); - session.close(); + } ); } @Test - public void testPessimisticLockWithDistinctThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); + public void testPessimisticLockWithDistinctThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - List products = - session.createQuery( - "select distinct p from Product p", - Product.class - ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .getResultList(); - - assertEquals( 50, products.size() ); - assertEquals( 51, sqlStatementInterceptor.getSqlQueries().size() ); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - session.getTransaction().commit(); - session.close(); - } + List products = session.createQuery( "select distinct p from Product p", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .getResultList(); - @Test - public void testPessimisticLockWithDistinctWhileExplicitlyDisablingFollowOnLockingThenFails() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); - - try { - List products = - session.createQuery( - "select distinct p from Product p where p.id > 40", - Product.class - ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( false ) ) - .getResultList(); - fail( "Should throw exception since Oracle does not support DISTINCT if follow on locking is disabled" ); - } - catch ( IllegalStateException expected ) { - assertEquals( - IllegalQueryOperationException.class, - expected.getCause().getClass() - ); - assertTrue( - expected.getCause().getMessage().contains( - "Locking with DISTINCT is not supported" - ) - ); - } + assertThat( products ).hasSize( 50 ); + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).doesNotContain( " for update " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " where tbl.id in (?," ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " for update " ); + } ); } @Test - public void testPessimisticLockWithDistinctWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); + public void testPessimisticLockWithDistinctWhileExplicitlyDisablingFollowOnLockingThenFails(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - sqlStatementInterceptor.getSqlQueries().clear(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - List products = - session.createQuery( - "select distinct p from Product p where p.id > 40" ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( true ) ) - .setMaxResults( 10 ) + try { + session.createQuery( "select distinct p from Product p where p.id > 40", Product.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) .getResultList(); - - assertEquals( 10, products.size() ); - assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() ); - - session.getTransaction().commit(); - session.close(); + fail( "Should throw exception since Oracle does not support DISTINCT if follow on locking is disabled" ); + } + catch ( IllegalStateException expected ) { + assertThat( expected.getCause() ).isInstanceOf( IllegalQueryOperationException.class ); + assertThat( expected.getCause().getMessage() ).contains( "Locking with DISTINCT is not supported" ); + } + + } ); } @Test - public void testPessimisticLockWithGroupByThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); + public void testPessimisticLockWithDistinctWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - sqlStatementInterceptor.getSqlQueries().clear(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - List products = - session.createQuery( - "select count(p), p " + - "from Product p " + - "group by p.id, p.name, p.vehicle.id " ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .getResultList(); - - assertEquals( 50, products.size() ); - assertEquals( 51, sqlStatementInterceptor.getSqlQueries().size() ); + List products = session.createQuery( "select distinct p from Product p where p.id > 40" ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.FORCE ) + .setMaxResults( 10 ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( products ).hasSize( 10 ); + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).doesNotContain( " for update " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " where tbl.id in (?," ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " for update " ); + } ); } @Test - public void testPessimisticLockWithGroupByWhileExplicitlyDisablingFollowOnLockingThenFails() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); - - try { - List products = - session.createQuery( - "select count(p), p " + - "from Product p " + - "group by p.id, p.name, p.vehicle.id " ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( false ) ) - .getResultList(); - fail( "Should throw exception since Oracle does not support GROUP BY if follow on locking is disabled" ); - } - catch ( IllegalStateException expected ) { - assertEquals( - IllegalQueryOperationException.class, - expected.getCause().getClass() - ); - assertTrue( - expected.getCause().getMessage().contains( - "Locking with GROUP BY is not supported" - ) - ); - } + public void testPessimisticLockWithGroupByThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + + final String qry = """ + select count(p), p \ + from Product p \ + group by p.id, p.name, p.vehicle.id"""; + List products = session.createQuery( qry, Object[].class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .getResultList(); + + assertThat( products ).hasSize( 50 ); + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).doesNotContain( " for update " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " where tbl.id in (?," ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " for update " ); + } ); } @Test - public void testPessimisticLockWithGroupByWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); - - sqlStatementInterceptor.getSqlQueries().clear(); - - List products = - session.createQuery( - "select count(p), p " + - "from Product p " + - "group by p.id, p.name, p.vehicle.id " ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) - .setFollowOnLocking( true ) ) + public void testPessimisticLockWithGroupByWhileExplicitlyDisablingFollowOnLockingThenFails(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + + try { + final String qry = """ + select count(p), p \ + from Product p \ + group by p.id, p.name, p.vehicle.id"""; + List products = session.createQuery( qry, Object[].class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) .getResultList(); - - assertEquals( 50, products.size() ); - assertEquals( 51, sqlStatementInterceptor.getSqlQueries().size() ); - - session.getTransaction().commit(); - session.close(); + fail( "Should throw exception since Oracle does not support GROUP BY if follow on locking is disabled" ); + } + catch ( IllegalStateException expected ) { + assertThat( expected.getCause() ).isInstanceOf( IllegalQueryOperationException.class ); + assertThat( expected.getCause().getMessage() ).contains( "Locking with GROUP BY is not supported" ); + } + } ); } @Test - public void testPessimisticLockWithUnionThenFollowOnLocking() { - - final Session session = openSession(); - session.beginTransaction(); + public void testPessimisticLockWithGroupByWhileExplicitlyEnablingFollowOnLockingThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + + final String qry = """ + select count(p), p \ + from Product p \ + group by p.id, p.name, p.vehicle.id"""; + List products = session.createQuery( qry, Object[].class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.FORCE ) + .getResultList(); + assertThat( products ).hasSize( 50 ); + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).doesNotContain( " for update " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " where tbl.id in (?," ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " for update " ); + } ); + } - sqlStatementInterceptor.getSqlQueries().clear(); + @Test + public void testPessimisticLockWithUnionThenFollowOnLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - List vehicles = session.createQuery( "select v from Vehicle v" ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) ) - .getResultList(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - assertEquals( 3, vehicles.size() ); - assertEquals( 4, sqlStatementInterceptor.getSqlQueries().size() ); + List vehicles = session.createQuery( "select v from Vehicle v", Vehicle.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .getResultList(); - session.getTransaction().commit(); - session.close(); + assertThat( vehicles ).hasSize( 3 ); + vehicles.forEach( (vehicle) -> { + assertThat( session.getCurrentLockMode( vehicle ) ).isEqualTo( PESSIMISTIC_WRITE ); + } ); + // follow on locking due to the UNION - initial query, lock trucks, lock sports cars + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " union all " ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).doesNotContain( " for update " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " where tbl.id in (?" ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).contains( " for update " ); + assertThat( sqlCollector.getSqlQueries().get( 2 ) ).contains( " where tbl.id in (?" ); + assertThat( sqlCollector.getSqlQueries().get( 2 ) ).contains( " for update " ); + } ); } @Test - public void testPessimisticLockWithUnionWhileExplicitlyDisablingFollowOnLockingThenFails() { - - final Session session = openSession(); - session.beginTransaction(); + public void testPessimisticLockWithUnionWhileExplicitlyDisablingFollowOnLockingThenFails(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - sqlStatementInterceptor.getSqlQueries().clear(); + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); - try { - List vehicles = session.createQuery( "select v from Vehicle v" ) - .setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ).setFollowOnLocking( false ) ) - .getResultList(); - fail( "Should throw exception since Oracle does not support UNION if follow on locking is disabled" ); - } - catch ( IllegalStateException expected ) { - assertEquals( - IllegalQueryOperationException.class, - expected.getCause().getClass() - ); - assertThat( expected.getCause().getMessage() ) - .contains( "Locking with set operators is not supported" ); - } + try { + List vehicles = session.createQuery( "select v from Vehicle v", Vehicle.class ) + .setHibernateLockMode( PESSIMISTIC_WRITE ) + .setFollowOnStrategy( Locking.FollowOn.DISALLOW ) + .getResultList(); + fail( "Should throw exception since Oracle does not support UNION if follow on locking is disabled" ); + } + catch ( IllegalStateException expected ) { + assertThat( expected.getCause() ).isInstanceOf( IllegalQueryOperationException.class ); + assertThat( expected.getCause().getMessage() ).contains( "Locking with set operators is not supported" ); + } + } ); } @NamedQuery( @@ -602,4 +568,53 @@ public static class Truck extends Vehicle { private double torque; } + + @Entity(name="Customer") + @Table(name="customers") + public static class Customer { + @Id + private Integer id; + private String name; + @OneToMany(mappedBy = "customer") + private Set purchases; + + public Customer() { + } + + public Customer(Integer id, String name) { + this.id = id; + this.name = name; + } + + public void addPurchase(Purchase purchase) { + if ( purchases == null ) { + purchases = new HashSet<>(); + } + purchases.add( purchase ); + } + } + + @Entity(name="Purchase") + @Table(name="purchases") + public static class Purchase { + @Id + private Integer id; + @Column(name = "ts") + private Instant timestamp; + private double amount; + @ManyToOne(optional = false) + private Customer customer; + + public Purchase() { + } + + public Purchase(Integer id, Instant timestamp, double amount, Customer customer) { + this.id = id; + this.timestamp = timestamp; + this.amount = amount; + this.customer = customer; + + customer.addPurchase( this ); + } + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ejb3configuration/PersisterClassProviderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ejb3configuration/PersisterClassProviderTest.java index 7330c3be281c..0b21d4d61b5c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ejb3configuration/PersisterClassProviderTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ejb3configuration/PersisterClassProviderTest.java @@ -189,6 +189,11 @@ public TableDetails getIdentifierTableDetails() { throw new UnsupportedOperationException(); } + @Override + public void forEachTableDetails(Consumer consumer) { + throw new UnsupportedOperationException(); + } + @Override public ModelPart findSubPart( String name, EntityMappingType targetType) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/legacy/CustomPersister.java b/hibernate-core/src/test/java/org/hibernate/orm/test/legacy/CustomPersister.java index e1e33f51d697..bef2f32c6ecd 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/legacy/CustomPersister.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/legacy/CustomPersister.java @@ -148,6 +148,11 @@ public TableDetails getIdentifierTableDetails() { throw new UnsupportedOperationException(); } + @Override + public void forEachTableDetails(Consumer consumer) { + throw new UnsupportedOperationException(); + } + @Override public ModelPart findSubPart( String name, EntityMappingType targetType) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/ExplicitLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/ExplicitLockingTest.java index 50bf29df9f95..f2350a17a099 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/ExplicitLockingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/ExplicitLockingTest.java @@ -19,6 +19,7 @@ import org.hibernate.Timeouts; import org.hibernate.dialect.OracleDialect; import org.hibernate.query.Query; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.Jpa; @@ -31,6 +32,7 @@ import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -42,7 +44,8 @@ annotatedClasses = { ExplicitLockingTest.Person.class, ExplicitLockingTest.Phone.class, - } + }, + useCollectingStatementInspector = true ) public class ExplicitLockingTest { @@ -101,6 +104,8 @@ public void testJPALockScope(EntityManagerFactoryScope scope) { @Test public void testSessionLock(EntityManagerFactoryScope scope) { + final SQLStatementInspector sqlCollector = scope.getCollectingStatementInspector(); + Person p = scope.fromTransaction( entityManager -> { log.info("testSessionLock"); Person person = new Person("John Doe"); @@ -121,6 +126,7 @@ public void testSessionLock(EntityManagerFactoryScope scope) { //end::locking-session-lock-example[] }); + sqlCollector.clear(); scope.inTransaction( entityManager -> { Long id = p.getId(); //tag::locking-session-lock-scope-example[] @@ -128,6 +134,8 @@ public void testSessionLock(EntityManagerFactoryScope scope) { Session session = entityManager.unwrap(Session.class); session.lock(person, LockMode.PESSIMISTIC_READ, Timeouts.NO_WAIT, PessimisticLockScope.EXTENDED); //end::locking-session-lock-scope-example[] + // find, lock persons, lock phones + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); }); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java index 689fa4c5f197..146ccd40896d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java @@ -8,6 +8,7 @@ import org.hibernate.Timeouts; import org.hibernate.community.dialect.GaussDBDialect; import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.SybaseDialect; import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; import org.hibernate.dialect.lock.spi.LockingSupport; import org.hibernate.testing.orm.junit.DialectFeatureChecks; @@ -87,6 +88,11 @@ void testSkipLocked(SessionFactoryScope factoryScope) { dialectClass = MySQLDialect.class, reason = "The docs claim 0 is a valid value as 'no wait'; but in my testing, after setting to 0 we get back 1" ) + @SkipForDialect( + dialectClass = SybaseDialect.class, + matchSubTypes = true, + reason = "Sybase docs say no-wait is supported, but after setting no-wait -1 is returned. And it unfortunately does not fail setting as no-wait." + ) void testNoWait(SessionFactoryScope factoryScope) { // this is dependent on the Dialect's ConnectionLockTimeoutType factoryScope.inTransaction( (session) -> session.doWork( (conn) -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/FollowOnLockingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/FollowOnLockingTests.java index 52018cee55a8..38d2ac19cdcd 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/FollowOnLockingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/FollowOnLockingTests.java @@ -49,7 +49,7 @@ void testFindBaseline(SessionFactoryScope factoryScope) { session.find( Book.class, 1, LockModeType.PESSIMISTIC_WRITE ); assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), false, session.getDialect(), Helper.Table.BOOKS ); } ); } @@ -64,11 +64,11 @@ void testFindWithForced(SessionFactoryScope factoryScope) { if ( usesTableHints( session.getDialect() ) ) { // t-sql assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), false, session.getDialect(), Helper.Table.BOOKS ); } else { assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), true, session.getDialect(), Helper.Table.BOOKS ); } } ); } @@ -89,11 +89,11 @@ void testFindWithForcedAsHint(SessionFactoryScope factoryScope) { if ( usesTableHints( session.getDialect() ) ) { // t-sql assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), false, session.getDialect(), Helper.Table.BOOKS ); } else { assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), true, session.getDialect(), Helper.Table.BOOKS ); } } ); } @@ -110,11 +110,11 @@ void testFindWithForcedAsHintName(SessionFactoryScope factoryScope) { if ( usesTableHints( session.getDialect() ) ) { // t-sql assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), false, session.getDialect(), Helper.Table.BOOKS ); } else { assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), true, session.getDialect(), Helper.Table.BOOKS ); } } ); } @@ -131,11 +131,11 @@ void testFindWithForcedAsLegacyHint(SessionFactoryScope factoryScope) { if ( usesTableHints( session.getDialect() ) ) { // t-sql assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), false, session.getDialect(), Helper.Table.BOOKS ); } else { assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS ); + Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), true, session.getDialect(), Helper.Table.BOOKS ); } } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/Helper.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/Helper.java index b1ba393fcf60..3a63cc04c062 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/Helper.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/Helper.java @@ -12,6 +12,7 @@ import org.hibernate.dialect.lock.PessimisticLockStyle; import org.hibernate.dialect.lock.spi.LockingSupport; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.transaction.TransactionUtil; import java.util.Collections; import java.util.HashSet; @@ -96,7 +97,11 @@ public interface TableInformation { String getKeyColumnName(); default String getKeyColumnReference() { - return getTableAlias() + "." + getKeyColumnName(); + return getKeyColumnReference( getTableAlias() ); + } + + default String getKeyColumnReference(String tableAlias) { + return tableAlias + "." + getKeyColumnName(); } } @@ -141,9 +146,39 @@ public String getKeyColumnName() { case REPORT_LABELS -> "report_fk"; }; } + + public String getCheckColumnName() { + return switch ( this ) { + case BOOKS, REPORTS -> "title"; + case PERSONS, JOINED_REPORTER, PUBLISHER -> "name"; + case REPORT_LABELS -> "txt"; + case BOOK_GENRES -> "genre"; + case BOOK_AUTHORS -> "idx"; + }; + } + + public void checkLocked(Number keyValue, boolean expectedToBeLocked, SessionFactoryScope factoryScope) { + if ( this == BOOK_AUTHORS ) { + TransactionUtil.deleteRow( factoryScope, getTableName(), expectedToBeLocked ); + } + else { + TransactionUtil.assertRowLock( + factoryScope, + getTableName(), + getCheckColumnName(), + getKeyColumnName(), + keyValue, + expectedToBeLocked + ); + } + } } public static void checkSql(String sql, Dialect dialect, TableInformation... tablesFetched) { + checkSql( sql, false, dialect, tablesFetched ); + } + + public static void checkSql(String sql, boolean expectingFollowOn, Dialect dialect, TableInformation... tablesFetched) { // note: assume `tables` is in order final LockingSupport.Metadata lockingMetadata = dialect.getLockingSupport().getMetadata(); final PessimisticLockStyle pessimisticLockStyle = lockingMetadata.getPessimisticLockStyle(); @@ -164,7 +199,7 @@ else if ( rowLockStrategy == RowLockStrategy.TABLE ) { else { buffer.append( "," ); } - buffer.append( table.getTableAlias() ); + buffer.append( expectingFollowOn ? "tbl" : table.getTableAlias() ); } aliases = buffer.toString(); } @@ -179,7 +214,7 @@ else if ( rowLockStrategy == RowLockStrategy.TABLE ) { else { buffer.append( "," ); } - buffer.append( table.getKeyColumnReference() ); + buffer.append( expectingFollowOn ? table.getKeyColumnReference( "tbl") : table.getKeyColumnReference() ); } aliases = buffer.toString(); } @@ -191,7 +226,8 @@ else if ( rowLockStrategy == RowLockStrategy.TABLE ) { // Transact SQL (mssql, sybase) "table hint"-style locking final LockOptions lockOptions = new LockOptions( LockMode.PESSIMISTIC_WRITE ); for ( TableInformation table : tablesFetched ) { - final String booksTableReference = dialect.appendLockHint( lockOptions, table.getTableAlias() ); + final String tableAlias = expectingFollowOn ? "tbl" : table.getTableAlias(); + final String booksTableReference = dialect.appendLockHint( lockOptions, tableAlias ); assertThat( sql ).contains( booksTableReference ); } } 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 5dad9614859e..762b22e6d4c8 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 @@ -12,16 +12,13 @@ import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.lock.spi.OuterJoinLockingType; -import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.FailureExpected; 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.hibernate.testing.orm.junit.SkipForDialect; -import org.hibernate.testing.orm.transaction.TransactionUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; @@ -31,8 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.LockMode.PESSIMISTIC_WRITE; 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; -import static org.hibernate.orm.test.locking.options.Helper.Table.JOINED_REPORTER; import static org.hibernate.orm.test.locking.options.Helper.Table.PERSONS; import static org.hibernate.orm.test.locking.options.Helper.Table.REPORTS; import static org.hibernate.orm.test.locking.options.Helper.Table.REPORT_LABELS; @@ -58,56 +55,29 @@ void dropTestData(SessionFactoryScope factoryScope) { factoryScope.dropData(); } - // todo : generally, we do not lock collection tables - HHH-19513 plus maybe general problem with many-to-many tables - @Test @SkipForDialect(dialectClass = InformixDialect.class, reason = "update does not block") void testFind(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); final Book theTalisman = session.find( Book.class, 3, PESSIMISTIC_WRITE ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false ); + + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), false, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), false, factoryScope ); } ); } @Test @SkipForDialect(dialectClass = InformixDialect.class, reason = "update does not block") void testFindWithExtended(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - - // note that this is not strictly spec compliant as it says EXTENDED should extend the locks to the `book_genres` table... factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); final Book theTalisman = session.find( Book.class, 3, PESSIMISTIC_WRITE, EXTENDED ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - // For strict compliance, EXTENDED here should lock `book_genres` but we do not - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false ); - } ); - } - @Test - @FailureExpected(reason = "See https://hibernate.atlassian.net/browse/HHH-19336?focusedCommentId=121552") - void testFindWithExtendedJpaExpectation(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - - factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); - final Book theTalisman = session.find( Book.class, 3, PESSIMISTIC_WRITE, EXTENDED ); - assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - // these 2 assertions would depend a bit on the approach and/or dialect -// assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); -// Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), Helper.Table.BOOK_GENRES ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), true ); + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), true, factoryScope ); } ); } @@ -116,12 +86,10 @@ void testFindWithExtendedJpaExpectation(SessionFactoryScope factoryScope) { @SkipForDialect(dialectClass = H2Dialect.class, reason = "H2 seems to not extend locks across joins") @SkipForDialect(dialectClass = InformixDialect.class, reason = "Cursor must be on simple SELECT for FOR UPDATE") void testFindWithExtendedAndFetch(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); // note that this is not strictly spec compliant as it says EXTENDED should extend // the locks to the `book_genres` table... factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); final Book theTalisman = session.find( Book.class, 3, @@ -131,99 +99,70 @@ void testFindWithExtendedAndFetch(SessionFactoryScope factoryScope) { ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - if ( session.getDialect().supportsOuterJoinForUpdate() ) { - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS, BOOK_GENRES ); - - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), true ); - } - else { - // should be 3, but follow-on locking is not locking collection tables... - // todo : track this down - HHH-19513 - assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), BOOKS ); - - // todo : track this down - HHH-19513 - //Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), BOOK_GENRES ); - //TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), true ); - } + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), true, factoryScope ); } ); } @Test @SkipForDialect(dialectClass = InformixDialect.class, reason = "update does not block") void testLock(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - factoryScope.inTransaction( (session) -> { final Book theTalisman = session.find( Book.class, 3 ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - sqlCollector.clear(); session.lock( theTalisman, PESSIMISTIC_WRITE ); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false ); + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), false, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), false, factoryScope ); } ); } @Test @SkipForDialect(dialectClass = InformixDialect.class, reason = "update does not block") void testLockWithExtended(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - factoryScope.inTransaction( (session) -> { final Book theTalisman = session.find( Book.class, 3 ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - sqlCollector.clear(); session.lock( theTalisman, PESSIMISTIC_WRITE, EXTENDED ); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - // Again, for strict compliance, EXTENDED here should lock `book_genres` but we do not - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false ); + + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), true, factoryScope ); } ); } @Test @SkipForDialect(dialectClass = InformixDialect.class, reason = "update does not block") void testRefresh(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); factoryScope.inTransaction( (session) -> { final Book theTalisman = session.find( Book.class, 3 ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - sqlCollector.clear(); session.refresh( theTalisman, PESSIMISTIC_WRITE ); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false ); + + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), false, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), false, factoryScope ); } ); } @Test @SkipForDialect(dialectClass = InformixDialect.class, reason = "update does not block") void testRefreshWithExtended(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - factoryScope.inTransaction( (session) -> { final Book theTalisman = session.find( Book.class, 3 ); assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); - sqlCollector.clear(); session.refresh( theTalisman, PESSIMISTIC_WRITE, EXTENDED ); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), BOOKS ); - TransactionUtil.assertRowLock( factoryScope, BOOKS.getTableName(), "title", "id", theTalisman.getId(), true ); - // Again, for strict compliance, EXTENDED here should lock `book_genres` but we do not - TransactionUtil.assertRowLock( factoryScope, BOOK_GENRES.getTableName(), "genre", "book_fk", theTalisman.getId(), false ); + + BOOKS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_AUTHORS.checkLocked( theTalisman.getId(), true, factoryScope ); + BOOK_GENRES.checkLocked( theTalisman.getId(), true, factoryScope ); } ); } @@ -231,21 +170,28 @@ void testRefreshWithExtended(SessionFactoryScope factoryScope) { @SkipForDialect(dialectClass = HSQLDialect.class, reason = "See https://sourceforge.net/p/hsqldb/bugs/1734/") @SkipForDialect(dialectClass = InformixDialect.class, reason = "Cursor must be on simple SELECT for FOR UPDATE") void testEagerFind(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE ); - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), REPORTS ); - TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), willAggressivelyLockJoinedTables( session.getDialect() ) ); - TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), willAggressivelyLockJoinedTables( session.getDialect() ) ); + REPORTS.checkLocked( report.getId(), true, factoryScope ); + } ); + } + + @Test + @SkipForDialect(dialectClass = HSQLDialect.class, reason = "See https://sourceforge.net/p/hsqldb/bugs/1734/") + @SkipForDialect(dialectClass = H2Dialect.class, reason = "H2 seems to not extend locks across joins") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Cursor must be on simple SELECT for FOR UPDATE") + void testEagerFindWithExtended(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, EXTENDED ); + + REPORTS.checkLocked( report.getId(), true, factoryScope ); + PERSONS.checkLocked( report.getReporter().getId(), willAggressivelyLockJoinedTables( session.getDialect() ), factoryScope ); + REPORT_LABELS.checkLocked( report.getId(), true, factoryScope ); } ); } private boolean willAggressivelyLockJoinedTables(Dialect dialect) { - // true when we have something like: + // Will be true when we have something like: // // select ... // from books b @@ -253,81 +199,21 @@ private boolean willAggressivelyLockJoinedTables(Dialect dialect) { // for update /// // and the database extends for-update to `persons` - // - // todo : this is something we should consider and disallow the situation return dialect.getLockingSupport().getMetadata().getOuterJoinLockingType() == OuterJoinLockingType.FULL; } - @Test - @SkipForDialect(dialectClass = HSQLDialect.class, reason = "See https://sourceforge.net/p/hsqldb/bugs/1734/") - @SkipForDialect(dialectClass = H2Dialect.class, reason = "H2 seems to not extend locks across joins") - @SkipForDialect(dialectClass = InformixDialect.class, reason = "Cursor must be on simple SELECT for FOR UPDATE") - void testEagerFindWithExtended(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - - factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); - final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, EXTENDED ); - if ( session.getDialect().supportsOuterJoinForUpdate() ) { - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), REPORTS, REPORT_LABELS ); - TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), - willAggressivelyLockJoinedTables( session.getDialect() ) ); - TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true ); - } - else { - assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), REPORTS ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), PERSONS ); - TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true ); - - // these should happen but currently do not - follow-on locking is not locking element-collection tables... - // todo : track this down - HHH-19513 - //Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), REPORT_LABELS ); - //TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true ); - - // this one should not happen at all. follow-on locking is not understanding scope probably.. - // todo : track this down - HHH-19514 - TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), true ); - } - } ); - } - @Test @SkipForDialect(dialectClass = HSQLDialect.class, reason = "See https://sourceforge.net/p/hsqldb/bugs/1734/") @SkipForDialect(dialectClass = H2Dialect.class, reason = "H2 seems to not extend locks across joins") @SkipForDialect(dialectClass = InformixDialect.class, reason = "Cursor must be on simple SELECT for FOR UPDATE") void testEagerFindWithFetchScope(SessionFactoryScope factoryScope) { - final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); - factoryScope.inTransaction( (session) -> { - sqlCollector.clear(); final Report report = session.find( Report.class, 2, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES ); - if ( session.getDialect().supportsOuterJoinForUpdate() ) { - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 0 ), session.getDialect(), REPORTS, REPORT_LABELS, JOINED_REPORTER ); - TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true ); - TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), true ); - TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true ); - } - else { - assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), REPORTS ); - Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), PERSONS ); - TransactionUtil.assertRowLock( factoryScope, REPORTS.getTableName(), "title", "id", report.getId(), true ); - - // these should happen but currently do not - follow-on locking is not locking element-collection tables... - // todo : track this down - HHH-19513 - //Helper.checkSql( sqlCollector.getSqlQueries().get( 2 ), session.getDialect(), REPORT_LABELS ); - //TransactionUtil.assertRowLock( factoryScope, REPORT_LABELS.getTableName(), "txt", "report_fk", report.getId(), true ); - - // this one should not happen at all. follow-on locking is not understanding scope probably.. - // todo : track this down - HHH-19514 - TransactionUtil.assertRowLock( factoryScope, PERSONS.getTableName(), "name", "id", report.getReporter().getId(), true ); - } + REPORTS.checkLocked( report.getId(), true, factoryScope ); + PERSONS.checkLocked( report.getReporter().getId(), true, factoryScope ); + REPORT_LABELS.checkLocked( report.getId(), true, factoryScope ); } ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/SmokeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/SmokeTests.java index 23b9044efb0b..5341fd75722d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/SmokeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/SmokeTests.java @@ -24,7 +24,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.basic.BasicResult; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/op/FollowOnLockingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/op/FollowOnLockingTests.java new file mode 100644 index 000000000000..e1d624408a70 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/op/FollowOnLockingTests.java @@ -0,0 +1,655 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.sql.exec.op; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.Table; +import org.hibernate.Locking; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.lock.PessimisticLockStyle; +import org.hibernate.testing.jdbc.SQLStatementInspector; +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.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static jakarta.persistence.LockModeType.PESSIMISTIC_WRITE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.Locking.FollowOn.FORCE; +import static org.hibernate.Locking.Scope.ROOT_ONLY; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel( annotatedClasses = { + FollowOnLockingTests.Name.class, + FollowOnLockingTests.Person.class, + FollowOnLockingTests.Post.class, + FollowOnLockingTests.Team.class, + FollowOnLockingTests.Customer.class +} ) +@SessionFactory(useCollectingStatementInspector = true) +public class FollowOnLockingTests { + + /** + * Performs follow-on locking against an entity (Person) with no associations + */ + @Test + void testSimpleLockScopeCases(SessionFactoryScope factoryScope) { + createTeamsData( factoryScope ); + + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // find() + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Person.class, 1, PESSIMISTIC_WRITE, ROOT_ONLY, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_COLLECTIONS + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Person.class, 1, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_COLLECTIONS, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Person.class, 1, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Query + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Person" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( ROOT_ONLY ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_COLLECTIONS + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Person" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_COLLECTIONS ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Person" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_FETCHES ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + } + + private boolean usesTableHints(Dialect dialect) { + return dialect.getLockingSupport().getMetadata().getPessimisticLockStyle() == PessimisticLockStyle.TABLE_HINT; + } + + /** + * Performs follow-on locking against an entity (Team) with a plural attribute + */ + @Test + void testCollectionLockScopeCases(SessionFactoryScope factoryScope) { + createTeamsData( factoryScope ); + + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // find() + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Team.class, 1, PESSIMISTIC_WRITE, ROOT_ONLY, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock teams + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_COLLECTIONS + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Team.class, 1, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_COLLECTIONS, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + // the initial query (w/ lock teams), lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + else { + // the initial query, lock teams, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Team.class, 1, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock teams + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Query + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Team" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( ROOT_ONLY ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock teams + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_COLLECTIONS + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Team" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_COLLECTIONS ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + // the initial query (w/ lock teams), lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + else { + // the initial query, lock teams, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Team" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_FETCHES ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock teams + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Team join fetch members" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_FETCHES ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock teams, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + } + + /** + * Performs follow-on locking against an entity (Post) with a to-one (it also has an element-collection) + */ + @Test + void testToOneCases(SessionFactoryScope factoryScope) { + createPostsData( factoryScope ); + + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // find() + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Post.class, 1, PESSIMISTIC_WRITE, ROOT_ONLY, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock posts + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_COLLECTIONS + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Post.class, 1, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_COLLECTIONS, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + // the initial query (w/ lock posts), lock tags + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + else { + // the initial query, lock posts, lock tags + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Post.class, 1, PESSIMISTIC_WRITE, Locking.Scope.INCLUDE_FETCHES, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock posts + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Query + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Post" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( ROOT_ONLY ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock posts + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_COLLECTIONS + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Post" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_COLLECTIONS ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + // the initial query (w/ lock posts), lock tags + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + else { + // the initial query, lock posts, lock tags + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + + // with INCLUDE_FETCHES + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Post" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_FETCHES ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock posts + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } + } ); + + // with INCLUDE_FETCHES (with fetch) + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.createQuery( "from Post join fetch author" ) + .setLockMode( PESSIMISTIC_WRITE ) + .setLockScope( Locking.Scope.INCLUDE_FETCHES ) + .setFollowOnStrategy( FORCE ) + .list(); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock posts, lock tags + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + } + + /** + * Performs follow-on locking against an entity (Customer) with secondary tables + */ + @Test + void testSecondaryTables(SessionFactoryScope factoryScope) { + createCustomersData( factoryScope ); + + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + // #find + + // with ROOT_ONLY + sqlCollector.clear(); + factoryScope.inTransaction( (session) -> { + session.find( Customer.class, 1, PESSIMISTIC_WRITE, ROOT_ONLY, FORCE ); + if ( usesTableHints( session.getDialect() ) ) { + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + } + else { + // the initial query, lock customers, lock receivables + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + } + } ); + } + + private void createTeamsData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Person jalen = new Person( 1, "Jalen", "Hurts" ); + session.persist( jalen ); + + final Person saquon = new Person( 2, "Saquon", "Barkley" ); + session.persist( saquon ); + + final Person zack = new Person( 3, "Zack", "Baun" ); + session.persist( zack ); + + final Team team1 = new Team( 1, "Philadelphia Eagles" ); + team1.addMembers( jalen, saquon, zack ); + session.persist( team1 ); + } ); + } + + private void createPostsData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Person camus = new Person( 1, "Albert", "Camus" ); + session.persist( camus ); + + final Post post = new Post( 1, "Thoughts on The Stranger", "...", camus ); + post.addTags( "exciting", "riveting" ); + session.persist( post ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + + private void createCustomersData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Customer spacely = new Customer( 1, "Spacely Sprokets", "Out there", "1234", "Cosmo Spacely" ); + session.persist( spacely ); + } ); + } + + @Embeddable + public record Name(String first, String last) { + } + + @Entity(name="Person") + @Table(name="persons") + public static class Person { + @Id + private Integer id; + @Embedded + private Name name; + + public Person() { + } + + public Person(Integer id, Name name) { + this.id = id; + this.name = name; + } + + public Person(Integer id, String firstName, String lastName) { + this( id, new Name( firstName, lastName ) ); + } + } + + @Entity(name="Post") + @Table(name="posts") + public static class Post { + @Id + private Integer id; + private String title; + @Lob + private String body; + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "author_fk") + private Person author; + @ElementCollection + @CollectionTable(name = "tags", joinColumns = @JoinColumn(name = "post_fk")) + private Set tags; + + public Post() { + } + + public Post(Integer id, String title, String body, Person author) { + this.id = id; + this.title = title; + this.body = body; + this.author = author; + } + + public void addTags(String... tags) { + if ( this.tags == null ) { + this.tags = new HashSet<>(); + } + Collections.addAll( this.tags, tags ); + } + } + + @Entity(name="Team") + @Table(name="teams") + public static class Team { + @Id + private Integer id; + private String name; + @OneToMany + @JoinColumn(name = "team_fk") + private Set members; + + public Team() { + } + + public Team(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getMembers() { + return members; + } + + public void setMembers(Set members) { + this.members = members; + } + + public Team addMember(Person member) { + if ( members == null ) { + members = new HashSet<>(); + } + members.add( member ); + return this; + } + + public Team addMembers(Person... incoming) { + if ( members == null ) { + members = new HashSet<>(); + } + Collections.addAll( members, incoming ); + return this; + } + } + + @Entity(name="Customer") + @Table(name="customers") + @SecondaryTable(name = "receivables", pkJoinColumns = @PrimaryKeyJoinColumn(name = "customer_fk")) + public static class Customer { + @Id + private Integer id; + private String name; + private String location; + @Column(table = "receivables") + private String accountNumber; + @Column(table = "receivables") + private String billingEntity; + + public Customer() { + } + + public Customer(Integer id, String name, String location, String accountNumber, String billingEntity) { + this.id = id; + this.name = name; + this.location = location; + this.accountNumber = accountNumber; + this.billingEntity = billingEntity; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + } + + public String getBillingEntity() { + return billingEntity; + } + + public void setBillingEntity(String billingEntity) { + this.billingEntity = billingEntity; + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java index 1f359c72243f..7634fac22af4 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java @@ -152,6 +152,40 @@ private static R wrapInTransaction(SharedSessionContract session, T actio } } + public static void deleteRow(SessionFactoryScope factoryScope, String tableName, boolean expectingToBlock) { + try { + AsyncExecutor.executeAsync( 2, TimeUnit.SECONDS, () -> { + factoryScope.inTransaction( (session) -> { + final String sql = String.format( "delete from %s", tableName ); + //noinspection deprecation + session.createNativeQuery( sql ).executeUpdate(); + if ( expectingToBlock ) { + fail( "Expecting `delete from " + tableName + "` to block due to locks" ); + } + } ); + } ); + } + catch (AsyncExecutor.TimeoutException expected) { + if ( !expectingToBlock ) { + fail( "Expecting update to " + tableName + " to succeed, but failed due to async timeout (presumably due to locks)", expected ); + } + } + catch (RuntimeException re) { + if ( re.getCause() instanceof jakarta.persistence.LockTimeoutException + || re.getCause() instanceof org.hibernate.exception.LockTimeoutException ) { + if ( !expectingToBlock ) { + fail( "Expecting update to " + tableName + " to succeed, but failed due to async timeout (presumably due to locks)", re.getCause() ); + } + } + else if ( re.getCause() instanceof ConstraintViolationException cve ) { + throw cve; + } + else { + throw re; + } + } + } + public static void assertRowLock(SessionFactoryScope factoryScope, String tableName, String columnName, String idColumn, Number id, boolean expectingToBlock) { final Dialect dialect = factoryScope.getSessionFactory().getJdbcServices().getDialect(); final boolean skipLocked = dialect.getLockingSupport().getMetadata().supportsSkipLocked(); @@ -177,8 +211,8 @@ else if ( !expectingToBlock && resultSize == 0 ) { try { AsyncExecutor.executeAsync( 2, TimeUnit.SECONDS, () -> { factoryScope.inTransaction( (session) -> { - //noinspection deprecation final String sql = String.format( "update %s set %s = null", tableName, columnName ); + //noinspection deprecation session.createNativeQuery( sql ).executeUpdate(); if ( expectingToBlock ) { fail( "Expecting update to " + tableName + " to block dues to locks" );