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/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..093d2b963d1a 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 @@ -34,7 +34,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.JdbcSelectExecutor; import org.hibernate.sql.results.graph.basic.BasicResult; 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 48eb6d8e6cfc..d08d2e150c5c 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..3575395bf7c7 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; /** @@ -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..95fea408be8a 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,83 +233,149 @@ 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; } - public void breakDownKeyJdbcValues( - Object domainValue, - KeyValueConsumer valueConsumer, - SharedSessionContractImplementor session) { - identifierPart.forEachJdbcValue( - domainValue, - keyColumns, - valueConsumer, - (selectionIndex, keys, consumer, jdbcValue, jdbcMapping) -> consumer.consume( - jdbcValue, - keys.get( selectionIndex ) - ), - session - ); - } - @Override - public int getColumnCount() { - return keyColumns.size(); + public List getKeyColumns() { + return keyColumns; } @Override - public List getKeyColumns() { - return keyColumns; + public int getColumnCount() { + return getKeyColumns().size(); } @Override public KeyColumn getKeyColumn(int position) { - return keyColumns.get( position ); + return getKeyColumns().get( position ); } @Override public void forEachKeyColumn(KeyColumnConsumer consumer) { - for ( int i = 0; i < keyColumns.size(); i++ ) { - consumer.consume( i, keyColumns.get( i ) ); + for ( int i = 0; i < getKeyColumns().size(); i++ ) { + consumer.consume( i, getKeyColumns().get( i ) ); } } - public void forEachKeyColumn(Consumer keyColumnConsumer) { - keyColumns.forEach( keyColumnConsumer ); - } - @Override public int getJdbcTypeCount() { - return keyColumns.size(); + return getKeyColumns().size(); } @Override public SelectableMapping getSelectable(int columnIndex) { - return keyColumns.get( columnIndex ); + return getKeyColumns().get( columnIndex ); } @Override public int forEachSelectable(int offset, SelectableConsumer consumer) { - for ( int i = 0; i < keyColumns.size(); i++ ) { - consumer.accept( i, keyColumns.get( i ) ); + 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, + getKeyColumns(), + valueConsumer, + (selectionIndex, keys, consumer, jdbcValue, jdbcMapping) -> consumer.consume( + jdbcValue, + keys.get( selectionIndex ) + ), + session + ); + } + + 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() + ); + } + } + + public static class SimpleKeyMapping extends AbstractKeyMapping { + private final KeyColumn keyColumn; + + public SimpleKeyMapping(List keyColumns, BasicValuedModelPart identifierPart) { + super( keyColumns, identifierPart ); + this.keyColumn = keyColumns.get( 0 ); + } + + @Override + 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() + ); + } + } + + public static class CompositeKeyMapping extends AbstractKeyMapping { + public CompositeKeyMapping(List keyColumns, EmbeddableValuedModelPart identifierPart) { + super( keyColumns, identifierPart ); + } + + @Override + 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" ); + } } public static class KeyColumn implements TableDetails.KeyColumn { 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..41c4091c46d6 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 @@ -9,6 +9,7 @@ import java.util.List; import java.util.Set; +import org.hibernate.LockOptions; import org.hibernate.ScrollMode; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.EmptyScrollableResults; @@ -22,7 +23,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; @@ -30,6 +31,7 @@ import org.hibernate.sql.results.spi.ResultsConsumer; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; /** * Standard implementation of {@link SelectQueryPlan} for @@ -87,7 +89,7 @@ public T executeQuery(DomainQueryExecutionContext executionContext, ResultsC } final JdbcOperationQuerySelect jdbcSelect = new JdbcOperationQuerySelect( - sql, + sqlToUse( sql, executionContext ), jdbcParameterBinders, resultSetMapping, affectedTableNames @@ -104,6 +106,13 @@ public T executeQuery(DomainQueryExecutionContext executionContext, ResultsC ); } + private static String sqlToUse(String sql, DomainQueryExecutionContext executionContext) { + final LockOptions lockOptions = executionContext.getQueryOptions().getLockOptions(); + return lockOptions.getLockMode().isPessimistic() + ? executionContext.getSession().getDialect().applyLocksToSql( sql, lockOptions, emptyMap() ) + : sql; + } + @Override public List performList(DomainQueryExecutionContext executionContext) { final QueryOptions queryOptions = executionContext.getQueryOptions(); @@ -128,7 +137,7 @@ public List performList(DomainQueryExecutionContext executionContext) { } final JdbcOperationQuerySelect jdbcSelect = new JdbcOperationQuerySelect( - sql, + sqlToUse( sql, executionContext ), jdbcParameterBinders, resultSetMapping, affectedTableNames @@ -170,7 +179,7 @@ public ScrollableResultsImplementor performScroll(ScrollMode scrollMode, Doma } final JdbcOperationQuerySelect jdbcSelect = new JdbcOperationQuerySelect( - sql, + sqlToUse( sql, executionContext ), jdbcParameterBinders, resultSetMapping, affectedTableNames 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..698fe09683a3 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; @@ -39,10 +34,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 +53,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 +79,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 +97,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 +127,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 +160,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 +181,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 +380,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 +435,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 +452,7 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter buildInterpretation( + protected static CacheableSqmInterpretation buildInterpretation( SqmSelectStatement sqm, DomainParameterXref domainParameterXref, DomainQueryExecutionContext executionContext, @@ -527,7 +506,7 @@ private interface SqmInterpreter { T interpret( X context, DomainQueryExecutionContext executionContext, - CacheableSqmInterpretation sqmInterpretation, + CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings); } @@ -537,7 +516,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/LockTimeoutHandler.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/LockTimeoutHandler.java new file mode 100644 index 000000000000..e7179ecbd928 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/LockTimeoutHandler.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.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.StatementAccess; +import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.PreAction; + +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/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..d5bc15cf5d7a 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 @@ -30,6 +30,7 @@ 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; @@ -66,6 +67,9 @@ 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; @@ -84,12 +88,10 @@ public void registerRoot(TableGroup root) { } } - if ( rowLockStrategy != RowLockStrategy.NONE ) { - if ( rootsToLock == null ) { - rootsToLock = new HashSet<>(); - } - rootsToLock.add( root ); + if ( rootsToLock == null ) { + rootsToLock = new HashSet<>(); } + rootsToLock.add( root ); } @Override @@ -130,12 +132,10 @@ else if ( joinedGroup.getModelPart() instanceof EntityPersister entityMapping ) } } - if ( rowLockStrategy != RowLockStrategy.NONE ) { - if ( joinsToLock == null ) { - joinsToLock = new LinkedHashSet<>(); - } - joinsToLock.add( join ); + if ( joinsToLock == null ) { + joinsToLock = new LinkedHashSet<>(); } + joinsToLock.add( join ); } @Override @@ -149,6 +149,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 +176,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 ); 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..f741cd679709 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,9 @@ 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.LockTimeoutHandler; 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 +89,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; @@ -135,14 +175,15 @@ import org.hibernate.sql.exec.internal.AbstractJdbcParameter; import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcSelectWithActions; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; 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.internal.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.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQueryUpdate; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBinding; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -158,6 +199,10 @@ import org.hibernate.sql.model.internal.TableInsertStandard; import org.hibernate.sql.model.internal.TableUpdateCustomSql; import org.hibernate.sql.model.internal.TableUpdateStandard; +import org.hibernate.sql.exec.internal.lock.FollowOnLockingAction; +import org.hibernate.sql.exec.internal.LoadedValuesCollectorImpl; +import org.hibernate.sql.exec.spi.JdbcSelect; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.type.BasicType; @@ -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,101 @@ protected JdbcOperationQuerySelect translateSelect(SelectStatement selectStateme getOffsetParameter(), getLimitParameter() ); + + final boolean needsLockingActions = determineIfLockingActionsNeeded( + lockOptions, + lockingTarget, + getDialect() + ); + + if ( !needsLockingActions ) { + return jdbcSelect; + } + + final JdbcSelectWithActions.Builder builder = new JdbcSelectWithActions.Builder( jdbcSelect ); + + final LockingSupport lockingSupport = getDialect().getLockingSupport(); + final LockingSupport.Metadata lockingSupportMetadata = lockingSupport.getMetadata(); + + final LockTimeoutType lockTimeoutType = lockingSupportMetadata.getLockTimeoutType( lockOptions.getTimeout() ); + if ( lockTimeoutType == LockTimeoutType.CONNECTION ) { + builder.addSecondaryActionPair( new LockTimeoutHandler( + lockOptions.getTimeout(), + lockingSupport.getConnectionLockTimeoutStrategy() + ) ); + } + + applyFollowOnLockingActions( + lockOptions, + lockingTarget, + lockingClauseStrategy, + builder + ); + + return builder.build(); + } + + public boolean determineIfLockingActionsNeeded( + LockOptions lockOptions, + QuerySpec lockingTarget, + Dialect dialect) { + if ( lockOptions == null || !lockOptions.getLockMode().isPessimistic() ) { + return false; + } + + final LockingSupport lockingSupport = dialect.getLockingSupport(); + final LockingSupport.Metadata lockingSupportMetadata = lockingSupport.getMetadata(); + + final LockTimeoutType lockTimeoutType = lockingSupportMetadata.getLockTimeoutType( lockOptions.getTimeout() ); + if ( lockTimeoutType == LockTimeoutType.CONNECTION ) { + return true; + } + + final LockStrategy lockStrategy = determineLockingStrategy( lockingTarget, lockOptions.getFollowOnStrategy() ); + if ( lockStrategy == LockStrategy.FOLLOW_ON ) { + return true; + } + + return false; + } + + private void applyFollowOnLockingActions( + 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() + ) ); + } + + protected static LoadedValuesCollector 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 + ); + } } private JdbcValuesMappingProducer buildJdbcValuesMappingProducer(SelectStatement selectStatement) { 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..57c9243daa8a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectWithActions.java @@ -0,0 +1,275 @@ +/* + * 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.StatementAccess; +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.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 ) { + return; + } + + for ( int i = 0; i < postActions.length; i++ ) { + if ( succeeded || postActions[i].shouldRunAfterFail() ) { + postActions[i].performPostAction( jdbcStatementAccess, jdbcConnection, executionContext ); + } + } + } + + @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; + } + + public Builder setLoadedValuesCollector(LoadedValuesCollector loadedValuesCollector) { + this.loadedValuesCollector = loadedValuesCollector; + return this; + } + + public JdbcSelectWithActions build() { + if ( preActions == null && postActions == null ) { + return new JdbcSelectWithActions( primaryAction, loadedValuesCollector ); + } + 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/LoadedValuesCollectorImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/LoadedValuesCollectorImpl.java new file mode 100644 index 000000000000..69f910c6204e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/LoadedValuesCollectorImpl.java @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ModelPartContainer; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.LockingClauseStrategy; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.exec.spi.LoadedValuesCollector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * Standard implementation of LoadedValuesCollector, used mainly for follow-on locking support. + * + * @author Steve Ebersole + */ +public 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 = extractPathsToLock( lockingClauseStrategy ); + } + + 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; + } + + @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 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/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/CollectionTableHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionTableHelper.java new file mode 100644 index 000000000000..88bb09d780bb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionTableHelper.java @@ -0,0 +1,182 @@ +/* + * 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.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +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.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +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.results.spi.ListResultsConsumer; + +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 CollectionTableHelper { + 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 SimpleTableGroup tableGroup = new SimpleTableGroup( + tableReference, + keyTableName, + attributeMapping + ); + + 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 InListPredicate restriction = new InListPredicate( columnReference ); + querySpec.applyPredicate( restriction ); + + final int expectedParamCount = ownerDetailsMap.size() * keyDescriptor.getJdbcTypeCount(); + final JdbcParameterBindingsImpl parameterBindings = new JdbcParameterBindingsImpl( expectedParamCount ); + + if ( keyDescriptor.getJdbcTypeCount() == 1 ) { + applySimpleCollectionKeyTableLockRestrictions( + attributeMapping, + keyDescriptor, + restriction, + parameterBindings, + ownerDetailsMap, + executionContext.getSession() + ); + } + else { + applyCompositeCollectionKeyTableLockRestrictions( + attributeMapping, + keyDescriptor, + restriction, + parameterBindings, + ownerDetailsMap, + executionContext.getSession() + ); + } + + 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 void applyCompositeCollectionKeyTableLockRestrictions( + PluralAttributeMapping attributeMapping, + ForeignKeyDescriptor keyDescriptor, + InListPredicate restriction, + JdbcParameterBindingsImpl parameterBindings, + Map ownerDetailsMap, + SharedSessionContractImplementor session) { + throw new UnsupportedOperationException( "Not implemented yet" ); + } + + 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 ) + ); + } +} 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..326e7a4ac8ac --- /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; + +/** + * Record of details about an entity 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..d0d646ac454a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java @@ -0,0 +1,238 @@ +/* + * 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.Locking; +import org.hibernate.engine.spi.EffectiveEntityGraph; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.PersistenceContext; +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.internal.QueryOptionsImpl; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; +import org.hibernate.sql.exec.internal.BaseExecutionContext; +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.LoadedValuesCollector.LoadedEntityRegistration; +import org.hibernate.sql.exec.spi.PostAction; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +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 LoadedValuesCollector loadedValuesCollector; + private final LockMode lockMode; + private final Timeout lockTimeout; + private final Locking.Scope lockScope; + + public FollowOnLockingAction( + LoadedValuesCollector loadedValuesCollector, + LockMode lockMode, + Timeout lockTimeout, + Locking.Scope lockScope) { + this.loadedValuesCollector = loadedValuesCollector; + this.lockMode = lockMode; + this.lockTimeout = lockTimeout; + this.lockScope = lockScope; + } + + @Override + public void performPostAction( + StatementAccess jdbcStatementAccess, + Connection jdbcConnection, + ExecutionContext executionContext) { + logLoadedValues( loadedValuesCollector ); + + final SharedSessionContractImplementor session = executionContext.getSession(); + + // NOTE: we deal with effective graphs here to make sure 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 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 segment for each table for the entity (keyed by name) + final Map tableSegments = prepareTableSegments( 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 = 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 String tableExpression = attributeMapping.getContainingTableExpression(); + final TableSegment entityTableSegment = tableSegments.get( tableExpression ); + + // here we apply the selection for the attribute to the corresponding + // table-segment keeping track of the state array index for later. + entityTableSegment.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, so for now always lock them. + CollectionTableHelper.lockCollectionTable( + attribute, + lockMode, + lockTimeout, + entityDetailsMap, + executionContext + ); + } ); + } + + + // at this point, we have all the individual locking selects ready to go. + // execute them and process the results against `entityDetailsMap` - + // for each row updating "loaded state" and ultimately refreshing the + // entity instance state + + // NOTE: share the context between table-segments (perf) + final ExecutionContext lockingExecutionContext = buildLockingExecutionContext( session ); + tableSegments.forEach( (s, entityTableSegment) -> { + entityTableSegment.performActions( entityDetailsMap, lockingExecutionContext ); + } ); + } ); + } + finally { + // reset the effective graph to whatever it was when we started + effectiveEntityGraph.clear(); + session.getLoadQueryInfluencers().applyEntityGraph( initialGraph, initialSemantic ); + } + } + + @SuppressWarnings("removal") + private ExecutionContext buildLockingExecutionContext(SharedSessionContractImplementor session) { + final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl(); + lockingQueryOptions.getLockOptions().setLockMode( lockMode ); + lockingQueryOptions.getLockOptions().setTimeout( lockTimeout ); + return new BaseExecutionContext( session ) { + @Override + public QueryOptions getQueryOptions() { + return lockingQueryOptions; + } + }; + } + + /** + * Collect loaded entities by type + */ + private Map> segmentLoadedValues() { + final Map> map = new IdentityHashMap<>(); + segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map ); + segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map ); + if ( map.isEmpty() ) { + throw new AssertionFailure( "Expecting some values" ); + } + return map; + } + + private 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() ); + } ); + } + + private Map prepareTableSegments( + EntityMappingType entityMappingType, + List entityKeys, + SharedSessionContractImplementor session) { + final Map segments = new HashMap<>(); + entityMappingType.forEachTableDetails( (tableDetails) -> segments.put( + tableDetails.getTableName(), + new TableSegment( tableDetails, entityMappingType, entityKeys, session ) + ) ); + return segments; + } + + private 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; + } + + 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() ); + } ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingCreationStates.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingCreationStates.java new file mode 100644 index 000000000000..f80f3fe033d3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingCreationStates.java @@ -0,0 +1,235 @@ +/* + * 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; + +/** + * For use with {@linkplain TableSegment} for follow-on locking support. + * + * @author Steve Ebersole + */ +public class FollowOnLockingCreationStates + 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 FollowOnLockingCreationStates( + 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 FollowOnLockingCreationStates getSqlAstCreationState() { + return this; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SqlAstCreationState + + @Override + public SqlAstCreationContext getCreationContext() { + return sessionFactory.getSqlTranslationEngine(); + } + + @Override + public FollowOnLockingCreationStates getCurrentProcessingState() { + return this; + } + + @Override + public FollowOnLockingCreationStates 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/SimpleTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/SimpleTableGroup.java new file mode 100644 index 000000000000..7d08df584251 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/SimpleTableGroup.java @@ -0,0 +1,140 @@ +/* + * 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.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 used to describe a {@linkplain TableSegment}. + * + * @author Steve Ebersole + */ +public class SimpleTableGroup implements TableGroup { + private final TableReference tableReference; + private final String tableName; + private final ModelPartContainer modelPart; + private final NavigablePath navigablePath; + + private List tableGroupJoins; + + public SimpleTableGroup( + TableReference tableReference, + String tableName, + ModelPartContainer modelPart) { + this.tableReference = tableReference; + this.tableName = tableName; + this.modelPart = modelPart; + this.navigablePath = new NavigablePath( tableName ); + } + + @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/TableSegment.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableSegment.java new file mode 100644 index 000000000000..4541a76ba137 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/TableSegment.java @@ -0,0 +1,294 @@ +/* + * 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.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.predicate.InListPredicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +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.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.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; + + +/** + * Consolidates processing for follow-on locking related to a single table. + * + * @author Steve Ebersole + */ +public class TableSegment { + private final TableDetails tableDetails; + private final EntityMappingType entityMappingType; + + private final QuerySpec querySpec = new QuerySpec( true ); + + private final NavigablePath rootPath; + private final TableReference tableReference; + private final TableGroup tableGroup; + + private final FollowOnLockingCreationStates creationStates; + + private final List statePositions = new ArrayList<>(); + private final List resultHandlers = new ArrayList<>(); + private final List> domainResults = new ArrayList<>(); + + private final JdbcParameterBindings jdbcParameterBindings; + + public TableSegment( + 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.rootPath = new NavigablePath( tableDetails.getTableName() ); + this.tableReference = new NamedTableReference( tableDetails.getTableName(), "tbl" ); + this.entityMappingType = entityMappingType; + this.tableGroup = new SimpleTableGroup( tableReference, tableDetails.getTableName(), entityMappingType ); + + querySpec.getFromClause().addRoot( tableGroup ); + + creationStates = new FollowOnLockingCreationStates( + querySpec, + tableGroup, + entityMappingType.getEntityPersister().getFactory() + ); + + // add the key as the first result + domainResults.add( tableDetails.getKeyDetails().createDomainResult( + rootPath.append( "{key}" ), + tableReference, + 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, + tableGroup, + ForeignKeyDescriptor.PART_NAME, + creationStates + ); + resultHandler = new ToOneResultHandler( index, toOne ); + } + else { + domainResult = attributeMapping.createDomainResult( + attributePath, + tableGroup, + null, + creationStates + ); + resultHandler = new NonToOneResultHandler( index ); + } + domainResults.add( domainResult ); + statePositions.add( index ); + resultHandlers.add( resultHandler ); + } + + public void applyKeyRestrictions(List entityKeys, SharedSessionContractImplementor session) { + 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( tableReference, 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( tableReference, 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, ExecutionContext lockingExecutionContext) { + final SharedSessionContractImplementor session = lockingExecutionContext.getSession(); + 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 JdbcOperationQuerySelect jdbcOperation = translator.translate( jdbcParameterBindings, lockingExecutionContext.getQueryOptions() ); + + final JdbcSelectExecutor jdbcSelectExecutor = jdbcServices.getJdbcSelectExecutor(); + final List results = jdbcSelectExecutor.executeQuery( + jdbcOperation, + jdbcParameterBindings, + lockingExecutionContext, + 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) { + entityDetails.entry().getLoadedState()[statePosition] = stateValue; + entityDetails.key().getPersister().getAttributeMapping( statePosition ).setValue( entityDetails.instance(), 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() + ); + } + entityDetails.entry().getLoadedState()[statePosition] = 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..1a6a52bed296 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/LoadedValuesCollector.java @@ -0,0 +1,64 @@ +/* + * 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 { + void registerEntity( + NavigablePath navigablePath, + EntityMappingType entityDescriptor, + EntityKey entityKey); + + void registerCollection( + NavigablePath navigablePath, + PluralAttributeMapping collectionDescriptor, + CollectionKey collectionKey); + + interface LoadedPartRegistration { + NavigablePath navigablePath(); + ModelPart modelPart(); + } + + List getCollectedRootEntities(); + + List getCollectedNonRootEntities(); + + List getCollectedCollections(); + + record LoadedEntityRegistration( + NavigablePath navigablePath, + EntityMappingType entityDescriptor, + EntityKey entityKey) implements LoadedPartRegistration { + @Override + public EntityMappingType modelPart() { + return entityDescriptor(); + } + } + + 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 24ea600d0878..730626cb87fd 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,36 +4,34 @@ */ 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 static java.util.Collections.emptyMap; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + import static org.hibernate.engine.jdbc.JdbcLogging.JDBC_MESSAGE_LOGGER; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; @@ -42,7 +40,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 +48,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 +74,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,92 +92,32 @@ public DeferredResultSetAccess( queryOptions ); - final LockOptions lockOptions = queryOptions.getLockOptions(); - final JdbcLockStrategy jdbcLockStrategy = jdbcSelect.getLockStrategy(); - final String sqlWithLocking; - if ( hasLocking( jdbcLockStrategy, lockOptions ) ) { - usesFollowOnLocking = useFollowOnLocking( jdbcLockStrategy, sqlWithLimit, queryOptions, lockOptions, dialect ); - if ( usesFollowOnLocking ) { - handleFollowOnLocking( executionContext, lockOptions ); - sqlWithLocking = sqlWithLimit; - } - else { - sqlWithLocking = dialect.applyLocksToSql( sqlWithLimit, lockOptions, emptyMap() ); - } - } - else { - usesFollowOnLocking = false; - sqlWithLocking = sqlWithLimit; - } - - final boolean commentsEnabled = - executionContext.getSession().getFactory() - .getSessionFactoryOptions().isCommentsEnabled(); - finalSql = dialect.addSqlHintOrComment( sqlWithLocking, queryOptions, commentsEnabled ); + final boolean commentsEnabled = executionContext.getSession() + .getFactory() + .getSessionFactoryOptions() + .isCommentsEnabled(); + finalSql = dialect.addSqlHintOrComment( sqlWithLimit, queryOptions, commentsEnabled ); } } - private boolean needsLimitHandler(JdbcOperationQuerySelect jdbcSelect) { + private boolean needsLimitHandler(JdbcSelect jdbcSelect) { return limit != null && !limit.isEmpty() && !jdbcSelect.usesLimitParameters(); } - private static boolean hasLocking(JdbcLockStrategy jdbcLockStrategy, LockOptions 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 + * + * @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 ) ); } - private static boolean useFollowOnLocking( - JdbcLockStrategy jdbcLockStrategy, - String sql, - QueryOptions queryOptions, - LockOptions lockOptions, - Dialect dialect) { - assert lockOptions != null; - return switch ( jdbcLockStrategy ) { - case FOLLOW_ON -> true; - case AUTO -> interpretAutoLockStrategy( sql, queryOptions, lockOptions, dialect); - case NONE -> false; - }; - } - - private static boolean interpretAutoLockStrategy( - String sql, - QueryOptions queryOptions, - LockOptions lockOptions, - Dialect dialect) { - return switch ( lockOptions.getFollowOnStrategy() ) { - case ALLOW -> dialect.useFollowOnLocking( sql, queryOptions ); - case FORCE -> true; - case DISALLOW, IGNORE -> false; - }; - } - public LimitHandler getLimitHandler() { return limitHandler; } @@ -207,10 +143,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/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/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..5b54ab24cc34 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/op/FollowOnLockingTests.java @@ -0,0 +1,540 @@ +/* + * 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.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 ); + // 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 ); + // 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 ); + // 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(); + // 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(); + // 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(); + // the initial query, lock persons + assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 ); + } ); + } + + /** + * 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 ); + // 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 ); + // 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 ); + // 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(); + // 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(); + // 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(); + // 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(); + // 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 ); + // 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 ); + // 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 ); + // 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(); + // 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(); + // 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(); + // 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(); + // the initial query, lock posts, lock persons + 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 ); + // 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; + } + } +}