From 9d0743503c3cc9814f4ee54d0c05b4536dd1281b Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 13 Aug 2025 13:06:03 -0500 Subject: [PATCH 1/3] HHH-19602 - Adjust JdbcOperation to allow more-than-one statement --- .../internal/AbstractDatabaseOperation.java | 64 ++++ .../internal/DatabaseOperationSelectImpl.java | 169 ++++++++++ .../sql/exec/internal/JdbcAction.java | 32 ++ .../sql/exec/internal/StatementAccess.java | 63 ++++ .../sql/exec/spi/DatabaseOperation.java | 28 ++ .../exec/spi/DatabaseOperationMutation.java | 30 ++ .../sql/exec/spi/DatabaseOperationSelect.java | 43 +++ .../exec/spi/DatabaseOperationSmokeTest.java | 289 ++++++++++++++++++ 8 files changed, 718 insertions(+) create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java new file mode 100644 index 000000000000..5369da0817de --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.DatabaseOperation; + +import java.sql.Connection; +import java.util.List; + +/** + * @author Steve Ebersole + */ +public abstract class AbstractDatabaseOperation implements DatabaseOperation { + protected final JdbcAction[] preActions; + protected final JdbcAction[] postActions; + + public AbstractDatabaseOperation() { + this( null, null ); + } + + public AbstractDatabaseOperation(JdbcAction[] preActions, JdbcAction[] postActions) { + this.preActions = preActions; + this.postActions = postActions; + } + + protected void performPreActions( + StatementAccess statementAccess, + Connection jdbcConnection, + ExecutionContext executionContext) { + performActions( preActions, statementAccess, jdbcConnection, executionContext ); + } + + protected void performPostActions( + StatementAccess statementAccess, + Connection jdbcConnection, + ExecutionContext executionContext) { + performActions( postActions, statementAccess, jdbcConnection, executionContext ); + } + + private void performActions( + JdbcAction[] actions, + StatementAccess statementAccess, + Connection jdbcConnection, + ExecutionContext executionContext) { + if ( actions == null ) { + return; + } + + for ( int i = 0; i < actions.length; i++ ) { + actions[i].perform( statementAccess, jdbcConnection, executionContext ); + } + } + + protected static JdbcAction[] toArray(List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + return actions.toArray( new JdbcAction[0] ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java new file mode 100644 index 000000000000..1338510d9600 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +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.DatabaseOperationSelect; +import org.hibernate.sql.results.spi.ResultsConsumer; +import org.hibernate.sql.results.spi.RowTransformer; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Standard DatabaseOperationSelect implementation. + * + * @author Steve Ebersole + */ +public class DatabaseOperationSelectImpl + extends AbstractDatabaseOperation + implements DatabaseOperationSelect { + private final JdbcOperationQuerySelect primaryOperation; + + public DatabaseOperationSelectImpl(JdbcOperationQuerySelect primaryOperation) { + this( null, null, primaryOperation ); + } + + public DatabaseOperationSelectImpl( + JdbcAction[] preActions, + JdbcAction[] postActions, + JdbcOperationQuerySelect primaryOperation) { + super( preActions, postActions ); + this.primaryOperation = primaryOperation; + } + + @Override + public JdbcOperationQuerySelect getPrimaryOperation() { + return primaryOperation; + } + + @Override + public Set getAffectedTableNames() { + return primaryOperation.getAffectedTableNames(); + } + + @Override + public T execute( + Class resultType, + int expectedNumberOfRows, + JdbcSelectExecutor.StatementCreator statementCreator, + JdbcParameterBindings jdbcParameterBindings, + RowTransformer rowTransformer, + ResultsConsumer resultsConsumer, + ExecutionContext executionContext) { + if ( preActions == null && postActions == null ) { + return performPrimaryOperation( + resultType, + statementCreator, + jdbcParameterBindings, + rowTransformer, + resultsConsumer, + executionContext + ); + } + + final SharedSessionContractImplementor session = executionContext.getSession(); + final LogicalConnectionImplementor logicalConnection = session.getJdbcCoordinator().getLogicalConnection(); + final SessionFactoryImplementor sessionFactory = session.getSessionFactory(); + + final Connection connection = logicalConnection.getPhysicalConnection(); + final StatementAccess statementAccess = new StatementAccess( + connection, + logicalConnection, + sessionFactory + ); + + try { + try { + performPreActions( statementAccess, connection, executionContext ); + return performPrimaryOperation( + resultType, + statementCreator, + jdbcParameterBindings, + rowTransformer, + resultsConsumer, + executionContext + ); + } + finally { + performPostActions( statementAccess, connection, executionContext ); + } + } + finally { + statementAccess.release(); + } + } + + private T performPrimaryOperation( + Class resultType, + JdbcSelectExecutor.StatementCreator statementCreator, + JdbcParameterBindings jdbcParameterBindings, + RowTransformer rowTransformer, + ResultsConsumer resultsConsumer, + ExecutionContext executionContext) { + final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory(); + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + final JdbcSelectExecutor jdbcSelectExecutor = jdbcServices.getJdbcSelectExecutor(); + return jdbcSelectExecutor.executeQuery( + primaryOperation, + jdbcParameterBindings, + executionContext, + rowTransformer, + resultType, + statementCreator, + resultsConsumer + ); + } + + public static Builder builder(JdbcOperationQuerySelect primaryAction) { + return new Builder( primaryAction ); + } + + public static class Builder { + private final JdbcOperationQuerySelect primaryAction; + + private List preActions; + private List postActions; + + private Builder(JdbcOperationQuerySelect primaryAction) { + this.primaryAction = primaryAction; + } + + public Builder addPreAction(JdbcAction... actions) { + if ( preActions == null ) { + preActions = new ArrayList<>(); + } + Collections.addAll( preActions, actions ); + return this; + } + + public Builder addPostAction(JdbcAction... actions) { + if ( postActions == null ) { + postActions = new ArrayList<>(); + } + Collections.addAll( postActions, actions ); + return this; + } + + public DatabaseOperationSelectImpl build() { + if ( preActions == null && postActions == null ) { + return new DatabaseOperationSelectImpl( primaryAction ); + } + final JdbcAction[] preActions = toArray( this.preActions ); + final JdbcAction[] postActions = toArray( this.postActions ); + return new DatabaseOperationSelectImpl( preActions, postActions, primaryAction ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java new file mode 100644 index 000000000000..5cffc32847a9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.internal; + +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.DatabaseOperation; + +import java.sql.Connection; + +/** + * An action to be performed before or after the primary action of a DatabaseOperation. + * + * @see DatabaseOperation#getPrimaryOperation() + * + * @author Steve Ebersole + */ +public interface JdbcAction { + /** + * Perform the action. + *

+ * Generally the action should use the passed {@code jdbcStatement} 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 perform(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java new file mode 100644 index 000000000000..b9b406cf70ae --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java @@ -0,0 +1,63 @@ +/* + * 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 java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Steve Ebersole + */ +public class StatementAccess implements AutoCloseable { + private final Connection jdbcConnection; + private final LogicalConnection logicalConnection; + private final SessionFactoryImplementor factory; + + private Statement jdbcStatement; + + public StatementAccess(Connection jdbcConnection, LogicalConnection logicalConnection, SessionFactoryImplementor factory) { + this.jdbcConnection = jdbcConnection; + this.logicalConnection = logicalConnection; + this.factory = factory; + } + + 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" ); + } + } + } + + @Override + public void close() { + release(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java new file mode 100644 index 000000000000..16c7764f9006 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import java.util.Set; + +/** + * One-or-more {@link JdbcOperation operations} performed against the database using JDBC. + * + * @apiNote By design, we expect one of the underlying {@link JdbcOperation operations} to be a + * {@linkplain #getPrimaryOperation "primary operation"} along with zero-or-more support operations + * to be performed before and/or after the primary operation. + * + * @author Steve Ebersole + */ +public interface DatabaseOperation { + /** + * The primary operation for the group. + */ + JdbcOperation getPrimaryOperation(); + + /** + * The names of tables referenced or affected by this operation. + */ + Set getAffectedTableNames(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java new file mode 100644 index 000000000000..587ea50141aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import java.sql.PreparedStatement; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * {@linkplain DatabaseOperation} whose primary operation is a mutation. + * + * @author Steve Ebersole + */ +public interface DatabaseOperationMutation extends DatabaseOperation { + /** + * Perform the execution. + * + * @param statementCreator Creator for JDBC {@linkplain PreparedStatement statements}. + * @param jdbcParameterBindings Bindings for the JDBC parameters. + * @param expectationCheck Check used to verify the outcome of the mutation. + * @param executionContext Access to contextual information useful while executing. + */ + int execute( + Function statementCreator, + JdbcParameterBindings jdbcParameterBindings, + BiConsumer expectationCheck, + ExecutionContext executionContext); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java new file mode 100644 index 000000000000..5d0194f917bc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.exec.spi; + +import org.hibernate.sql.exec.spi.JdbcSelectExecutor.StatementCreator; +import org.hibernate.sql.results.spi.ResultsConsumer; +import org.hibernate.sql.results.spi.RowTransformer; + +import java.sql.PreparedStatement; + +/** + * {@linkplain DatabaseOperation} whose primary operation is a selection. + * + * @author Steve Ebersole + */ +public interface DatabaseOperationSelect extends DatabaseOperation { + @Override + JdbcOperationQuerySelect getPrimaryOperation(); + + /** + * Execute the underlying statements and return the result(s). + * + * @param resultType The expected type of domain result values. + * @param expectedNumberOfRows The number of domain results expected. + * @param statementCreator Creator for JDBC {@linkplain PreparedStatement statements}. + * @param jdbcParameterBindings Bindings for the JDBC parameters. + * @param rowTransformer Any row transformation to apply. + * @param resultsConsumer Consumer for each domain result. + * @param executionContext Access to contextual information useful while executing. + * + * @return The indicated result(s). + */ + T execute( + Class resultType, + int expectedNumberOfRows, + StatementCreator statementCreator, + JdbcParameterBindings jdbcParameterBindings, + RowTransformer rowTransformer, + ResultsConsumer resultsConsumer, + ExecutionContext executionContext); +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java new file mode 100644 index 000000000000..81663c5c3484 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java @@ -0,0 +1,289 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.sql.exec.spi; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Timeout; +import org.hibernate.ScrollMode; +import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; +import org.hibernate.dialect.lock.spi.LockingSupport; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.loader.ast.internal.LoaderSelectBuilder; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.internal.StandardStatementCreator; +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.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.internal.DatabaseOperationSelectImpl; +import org.hibernate.sql.exec.internal.JdbcAction; +import org.hibernate.sql.exec.internal.StatementAccess; +import org.hibernate.sql.results.spi.SingleResultConsumer; +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.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; + + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = DatabaseOperationSmokeTest.Person.class) +@SessionFactory +public class DatabaseOperationSmokeTest { + @BeforeEach + void createTestData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.persist( new Person( 1, "Steve ") ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + + @Test + void testSimpleSelect(SessionFactoryScope factoryScope) { + final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory(); + final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel().findEntityDescriptor( Person.class ); + + final PersonQuery personQuery = createPersonQuery( entityDescriptor, sessionFactory ); + final JdbcOperationQuerySelect jdbcOperation = personQuery.jdbcOperation(); + final JdbcParameterBindings jdbcParameterBindings = personQuery.jdbcParameterBindings(); + + final DatabaseOperationSelectImpl databaseOperation = new DatabaseOperationSelectImpl( jdbcOperation ); + + factoryScope.inTransaction( (session) -> { + final Person person = databaseOperation.execute( + Person.class, + 1, + StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ), + jdbcParameterBindings, + row -> (Person) row[0], + SingleResultConsumer.instance(), + new SingleIdExecutionContext( + session, + null, + 1, + entityDescriptor, + QueryOptions.NONE, + null + ) + ); + } ); + } + + @Test + void testConnectionLockTimeout(SessionFactoryScope factoryScope) { + final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory(); + + final LockingSupport lockingSupport = sessionFactory.getJdbcServices().getDialect().getLockingSupport(); + final ConnectionLockTimeoutStrategy lockTimeoutStrategy = lockingSupport.getConnectionLockTimeoutStrategy(); + if ( lockTimeoutStrategy.getSupportedLevel() == ConnectionLockTimeoutStrategy.Level.NONE ) { + return; + } + + final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel().findEntityDescriptor( Person.class ); + + final PersonQuery personQuery = createPersonQuery( entityDescriptor, sessionFactory ); + final JdbcOperationQuerySelect jdbcOperation = personQuery.jdbcOperation(); + final JdbcParameterBindings jdbcParameterBindings = personQuery.jdbcParameterBindings(); + + + final LockTimeoutSetter lockTimeoutSetter = new LockTimeoutSetter( Timeout.seconds( 2 ), lockTimeoutStrategy ); + final LockTimeoutResetter lockTimeoutResetter = new LockTimeoutResetter( lockTimeoutStrategy, lockTimeoutSetter ); + + final DatabaseOperationSelectImpl databaseOperation = DatabaseOperationSelectImpl.builder( jdbcOperation ) + .addPreAction( lockTimeoutSetter ) + .addPostAction( lockTimeoutResetter ) + .build(); + + factoryScope.inTransaction( (session) -> { + final Person person = databaseOperation.execute( + Person.class, + 1, + StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ), + jdbcParameterBindings, + row -> (Person) row[0], + SingleResultConsumer.instance(), + new SingleIdExecutionContext( + session, + null, + 1, + entityDescriptor, + QueryOptions.NONE, + null + ) + ); + } ); + } + + private static class LockTimeoutSetter implements JdbcAction { + private final ConnectionLockTimeoutStrategy lockTimeoutStrategy; + private final Timeout timeout; + private Timeout baseline; + + public LockTimeoutSetter(Timeout timeout, ConnectionLockTimeoutStrategy lockTimeoutStrategy) { + this.timeout = timeout; + this.lockTimeoutStrategy = lockTimeoutStrategy; + } + + public Timeout getBaseline() { + return baseline; + } + + @Override + public void perform(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + // first, get the baseline + baseline = lockTimeoutStrategy.getLockTimeout( jdbcConnection, factory ); + + // now set the timeout + lockTimeoutStrategy.setLockTimeout( timeout, jdbcConnection, factory ); + } + } + + private static class LockTimeoutResetter implements JdbcAction { + private final ConnectionLockTimeoutStrategy lockTimeoutStrategy; + private final LockTimeoutSetter setter; + + public LockTimeoutResetter(ConnectionLockTimeoutStrategy lockTimeoutStrategy, LockTimeoutSetter setter) { + this.lockTimeoutStrategy = lockTimeoutStrategy; + this.setter = setter; + } + + @Override + public void perform(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + // reset the timeout + lockTimeoutStrategy.setLockTimeout( setter.getBaseline(), jdbcConnection, factory ); + } + } + + + private PersonQuery createPersonQuery( + EntityPersister entityDescriptor, + SessionFactoryImplementor sessionFactory) { + final MutableObject jdbcParamRef = new MutableObject<>(); + final SelectStatement selectAst = LoaderSelectBuilder.createSelect( + entityDescriptor, + null, + entityDescriptor.getIdentifierMapping(), + null, + 1, + new LoadQueryInfluencers( sessionFactory ), + null, + jdbcParamRef::setIfNot, + sessionFactory + ); + + final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl( 1 ); + jdbcParameterBindings.addBinding( + jdbcParamRef.get(), + new JdbcParameterBindingImpl( + entityDescriptor.getIdentifierMapping().getJdbcMapping( 0 ), + 1 + ) + ); + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + final JdbcOperationQuerySelect jdbcOperation = jdbcServices + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator( sessionFactory, selectAst ) + .translate( jdbcParameterBindings, QueryOptions.NONE ); + + return new PersonQuery( jdbcOperation, jdbcParameterBindings ); + } + + private record PersonQuery( + JdbcOperationQuerySelect jdbcOperation, + JdbcParameterBindings jdbcParameterBindings) { + } + + @Entity(name="Person") + @Table(name="persons") + public static class Person { + @Id + private Integer id; + private String name; + + public Person() { + } + + public Person(Integer id, String name) { + this.id = id; + this.name = name; + } + } + + private static class SingleIdExecutionContext extends BaseExecutionContext { + private final Object entityInstance; + private final Object restrictedValue; + private final EntityMappingType rootEntityDescriptor; + private final QueryOptions queryOptions; + private final Callback callback; + + public SingleIdExecutionContext( + SharedSessionContractImplementor session, + Object entityInstance, + Object restrictedValue, + EntityMappingType rootEntityDescriptor, QueryOptions queryOptions, + Callback callback) { + super( session ); + this.entityInstance = entityInstance; + this.restrictedValue = restrictedValue; + this.rootEntityDescriptor = rootEntityDescriptor; + this.queryOptions = queryOptions; + this.callback = callback; + } + + @Override + public Object getEntityInstance() { + return entityInstance; + } + + @Override + public Object getEntityId() { + return restrictedValue; + } + + @Override + public EntityMappingType getRootEntityDescriptor() { + return rootEntityDescriptor; + } + + @Override + public QueryOptions getQueryOptions() { + return queryOptions; + } + + @Override + public Callback getCallback() { + return callback; + } + + } +} From 8337dabd47be0762bc941b0aaba6d514ccf5aa64 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 13 Aug 2025 15:00:19 -0500 Subject: [PATCH 2/3] HHH-19602 - Adjust JdbcOperation to allow more-than-one statement --- .../internal/AbstractDatabaseOperation.java | 130 +++++++++++++++--- .../internal/DatabaseOperationSelectImpl.java | 37 ++--- .../sql/exec/internal/JdbcAction.java | 1 + ...ntAccess.java => StatementAccessImpl.java} | 15 +- .../sql/exec/spi/DatabaseOperation.java | 11 +- .../exec/spi/DatabaseOperationMutation.java | 3 + .../sql/exec/spi/DatabaseOperationSelect.java | 2 + .../hibernate/sql/exec/spi/PostAction.java | 29 ++++ .../org/hibernate/sql/exec/spi/PreAction.java | 29 ++++ .../sql/exec/spi/SecondaryAction.java | 17 +++ .../sql/exec/spi/StatementAccess.java | 22 +++ .../exec/spi/DatabaseOperationSmokeTest.java | 33 ++--- 12 files changed, 249 insertions(+), 80 deletions(-) rename hibernate-core/src/main/java/org/hibernate/sql/exec/internal/{StatementAccess.java => StatementAccessImpl.java} (76%) create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java index 5369da0817de..a727d4a341aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java @@ -7,22 +7,30 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.DatabaseOperation; +import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.PreAction; +import org.hibernate.sql.exec.spi.SecondaryAction; +import org.hibernate.sql.exec.spi.StatementAccess; +import java.lang.reflect.Array; import java.sql.Connection; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** * @author Steve Ebersole */ +@SuppressWarnings("ALL") public abstract class AbstractDatabaseOperation implements DatabaseOperation { - protected final JdbcAction[] preActions; - protected final JdbcAction[] postActions; + protected final PreAction[] preActions; + protected final PostAction[] postActions; public AbstractDatabaseOperation() { this( null, null ); } - public AbstractDatabaseOperation(JdbcAction[] preActions, JdbcAction[] postActions) { + public AbstractDatabaseOperation(PreAction[] preActions, PostAction[] postActions) { this.preActions = preActions; this.postActions = postActions; } @@ -31,34 +39,118 @@ protected void performPreActions( StatementAccess statementAccess, Connection jdbcConnection, ExecutionContext executionContext) { - performActions( preActions, statementAccess, jdbcConnection, executionContext ); + if ( preActions != null ) { + for ( int i = 0; i < preActions.length; i++ ) { + preActions[i].performPreAction( statementAccess, jdbcConnection, executionContext ); + } + } } protected void performPostActions( StatementAccess statementAccess, Connection jdbcConnection, ExecutionContext executionContext) { - performActions( postActions, statementAccess, jdbcConnection, executionContext ); + if ( postActions != null ) { + for ( int i = 0; i < postActions.length; i++ ) { + postActions[i].performPostAction( statementAccess, jdbcConnection, executionContext ); + } + } } - private void performActions( - JdbcAction[] actions, - StatementAccess statementAccess, - Connection jdbcConnection, - ExecutionContext executionContext) { - if ( actions == null ) { - return; + protected static T[] toArray(Class type, List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; } + return actions.toArray( (T[]) Array.newInstance( type, 0 ) ); + } + + protected abstract static class Builder> { + protected List preActions; + protected List postActions; - for ( int i = 0; i < actions.length; i++ ) { - actions[i].perform( statementAccess, jdbcConnection, executionContext ); + protected abstract T getThis(); + + /** + * Appends the {@code actions} to the growing list of pre-actions + * + * @return {@code this}, for method chaining. + */ + public T appendPreAction(PreAction... actions) { + if ( preActions == null ) { + preActions = new ArrayList<>(); + } + Collections.addAll( preActions, actions ); + return getThis(); } - } - protected static JdbcAction[] toArray(List actions) { - if ( CollectionHelper.isEmpty( actions ) ) { - return null; + /** + * Prepends the {@code actions} to the growing list of pre-actions + * + * @return {@code this}, for method chaining. + */ + public T prependPreAction(PreAction... actions) { + if ( preActions == null ) { + preActions = new ArrayList<>(); + } + for ( int i = actions.length - 1; i >= 0; i-- ) { + preActions.add( 0, actions[i] ); + } + return getThis(); + } + + /** + * Appends the {@code actions} to the growing list of post-actions + * + * @return {@code this}, for method chaining. + */ + public T appendPostAction(PostAction... actions) { + if ( postActions == null ) { + postActions = new ArrayList<>(); + } + Collections.addAll( postActions, actions ); + return getThis(); + } + + /** + * Prepends the {@code actions} to the growing list of post-actions + * + * @return {@code this}, for method chaining. + */ + public T prependPostAction(PostAction... actions) { + if ( postActions == null ) { + postActions = new ArrayList<>(); + } + for ( int i = actions.length - 1; i >= 0; i-- ) { + postActions.add( 0, actions[i] ); + } + return getThis(); + } + + /** + * Adds a secondary action. Assumes the action implements both + * {@linkplain PreAction} and {@linkplain PostAction}. + * + * @see #prependPreAction + * @see #appendPostAction + * + * @return {@code this}, for method chaining. + */ + public T 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 T addSecondaryActionPair(PreAction preAction, PostAction postAction) { + prependPreAction( preAction ); + appendPostAction( postAction ); + return getThis(); } - return actions.toArray( new JdbcAction[0] ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java index 1338510d9600..a2bf683073de 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java @@ -8,18 +8,17 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; +import org.hibernate.sql.exec.spi.DatabaseOperationSelect; 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.DatabaseOperationSelect; +import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.PreAction; import org.hibernate.sql.results.spi.ResultsConsumer; import org.hibernate.sql.results.spi.RowTransformer; import java.sql.Connection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Set; /** @@ -37,8 +36,8 @@ public DatabaseOperationSelectImpl(JdbcOperationQuerySelect primaryOperation) { } public DatabaseOperationSelectImpl( - JdbcAction[] preActions, - JdbcAction[] postActions, + PreAction[] preActions, + PostAction[] postActions, JdbcOperationQuerySelect primaryOperation) { super( preActions, postActions ); this.primaryOperation = primaryOperation; @@ -79,7 +78,7 @@ public T execute( final SessionFactoryImplementor sessionFactory = session.getSessionFactory(); final Connection connection = logicalConnection.getPhysicalConnection(); - final StatementAccess statementAccess = new StatementAccess( + final StatementAccessImpl statementAccess = new StatementAccessImpl( connection, logicalConnection, sessionFactory @@ -131,29 +130,15 @@ public static Builder builder(JdbcOperationQuerySelect primaryAction) { return new Builder( primaryAction ); } - public static class Builder { + public static class Builder extends AbstractDatabaseOperation.Builder { private final JdbcOperationQuerySelect primaryAction; - private List preActions; - private List postActions; - private Builder(JdbcOperationQuerySelect primaryAction) { this.primaryAction = primaryAction; } - public Builder addPreAction(JdbcAction... actions) { - if ( preActions == null ) { - preActions = new ArrayList<>(); - } - Collections.addAll( preActions, actions ); - return this; - } - - public Builder addPostAction(JdbcAction... actions) { - if ( postActions == null ) { - postActions = new ArrayList<>(); - } - Collections.addAll( postActions, actions ); + @Override + protected Builder getThis() { return this; } @@ -161,8 +146,8 @@ public DatabaseOperationSelectImpl build() { if ( preActions == null && postActions == null ) { return new DatabaseOperationSelectImpl( primaryAction ); } - final JdbcAction[] preActions = toArray( this.preActions ); - final JdbcAction[] postActions = toArray( this.postActions ); + final PreAction[] preActions = toArray( PreAction.class, this.preActions ); + final PostAction[] postActions = toArray( PostAction.class, this.postActions ); return new DatabaseOperationSelectImpl( preActions, postActions, primaryAction ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java index 5cffc32847a9..de370033df26 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java @@ -6,6 +6,7 @@ import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.DatabaseOperation; +import org.hibernate.sql.exec.spi.StatementAccess; import java.sql.Connection; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java similarity index 76% rename from hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java rename to hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java index b9b406cf70ae..db8b85e9fa84 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java @@ -6,28 +6,32 @@ 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 StatementAccess implements AutoCloseable { +public class StatementAccessImpl implements StatementAccess { private final Connection jdbcConnection; private final LogicalConnection logicalConnection; private final SessionFactoryImplementor factory; private Statement jdbcStatement; - public StatementAccess(Connection jdbcConnection, LogicalConnection logicalConnection, SessionFactoryImplementor factory) { + public StatementAccessImpl(Connection jdbcConnection, LogicalConnection logicalConnection, SessionFactoryImplementor factory) { this.jdbcConnection = jdbcConnection; this.logicalConnection = logicalConnection; this.factory = factory; } - public Statement getJdbcStatement() { + @Override public Statement getJdbcStatement() { if ( jdbcStatement == null ) { try { jdbcStatement = jdbcConnection.createStatement(); @@ -55,9 +59,4 @@ public void release() { } } } - - @Override - public void close() { - release(); - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java index 16c7764f9006..60219e24a015 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java @@ -4,17 +4,18 @@ */ package org.hibernate.sql.exec.spi; +import org.hibernate.Incubating; + import java.util.Set; /** - * One-or-more {@link JdbcOperation operations} performed against the database using JDBC. - * - * @apiNote By design, we expect one of the underlying {@link JdbcOperation operations} to be a - * {@linkplain #getPrimaryOperation "primary operation"} along with zero-or-more support operations - * to be performed before and/or after the primary operation. + * An operation against the database, comprised of a single + * {@linkplain #getPrimaryOperation primary operation} and zero-or-more + * before/after {@linkplain SecondaryAction secondary actions}. * * @author Steve Ebersole */ +@Incubating public interface DatabaseOperation { /** * The primary operation for the group. diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java index 587ea50141aa..32a404c1b220 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java @@ -4,6 +4,8 @@ */ package org.hibernate.sql.exec.spi; +import org.hibernate.Incubating; + import java.sql.PreparedStatement; import java.util.function.BiConsumer; import java.util.function.Function; @@ -13,6 +15,7 @@ * * @author Steve Ebersole */ +@Incubating public interface DatabaseOperationMutation extends DatabaseOperation { /** * Perform the execution. diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java index 5d0194f917bc..66e3f2ee76ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java @@ -4,6 +4,7 @@ */ package org.hibernate.sql.exec.spi; +import org.hibernate.Incubating; import org.hibernate.sql.exec.spi.JdbcSelectExecutor.StatementCreator; import org.hibernate.sql.results.spi.ResultsConsumer; import org.hibernate.sql.results.spi.RowTransformer; @@ -15,6 +16,7 @@ * * @author Steve Ebersole */ +@Incubating public interface DatabaseOperationSelect extends DatabaseOperation { @Override JdbcOperationQuerySelect getPrimaryOperation(); 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..dc6a8a4cb0da --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.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 after a {@linkplain DatabaseOperation}'s primary operation. + */ +@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); +} 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..40332913bdd7 --- /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 DatabaseOperation}'s primary operation. + */ +@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/SecondaryAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java new file mode 100644 index 000000000000..9976e754fd36 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java @@ -0,0 +1,17 @@ +/* + * 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}, + * which are 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/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/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java index 81663c5c3484..e9a8630acf43 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java @@ -31,8 +31,9 @@ import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.internal.DatabaseOperationSelectImpl; -import org.hibernate.sql.exec.internal.JdbcAction; -import org.hibernate.sql.exec.internal.StatementAccess; +import org.hibernate.sql.exec.spi.PostAction; +import org.hibernate.sql.exec.spi.PreAction; +import org.hibernate.sql.exec.spi.StatementAccess; import org.hibernate.sql.results.spi.SingleResultConsumer; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; @@ -111,12 +112,10 @@ void testConnectionLockTimeout(SessionFactoryScope factoryScope) { final JdbcParameterBindings jdbcParameterBindings = personQuery.jdbcParameterBindings(); - final LockTimeoutSetter lockTimeoutSetter = new LockTimeoutSetter( Timeout.seconds( 2 ), lockTimeoutStrategy ); - final LockTimeoutResetter lockTimeoutResetter = new LockTimeoutResetter( lockTimeoutStrategy, lockTimeoutSetter ); + final LockTimeoutHandler lockTimeoutHandler = new LockTimeoutHandler( Timeout.seconds( 2 ), lockTimeoutStrategy ); final DatabaseOperationSelectImpl databaseOperation = DatabaseOperationSelectImpl.builder( jdbcOperation ) - .addPreAction( lockTimeoutSetter ) - .addPostAction( lockTimeoutResetter ) + .addSecondaryActionPair( lockTimeoutHandler ) .build(); factoryScope.inTransaction( (session) -> { @@ -139,12 +138,12 @@ void testConnectionLockTimeout(SessionFactoryScope factoryScope) { } ); } - private static class LockTimeoutSetter implements JdbcAction { + private static class LockTimeoutHandler implements PreAction, PostAction { private final ConnectionLockTimeoutStrategy lockTimeoutStrategy; private final Timeout timeout; private Timeout baseline; - public LockTimeoutSetter(Timeout timeout, ConnectionLockTimeoutStrategy lockTimeoutStrategy) { + public LockTimeoutHandler(Timeout timeout, ConnectionLockTimeoutStrategy lockTimeoutStrategy) { this.timeout = timeout; this.lockTimeoutStrategy = lockTimeoutStrategy; } @@ -154,32 +153,22 @@ public Timeout getBaseline() { } @Override - public void perform(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + public void performPreAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - // first, get the baseline + // first, get the baseline (for post-action) baseline = lockTimeoutStrategy.getLockTimeout( jdbcConnection, factory ); // now set the timeout lockTimeoutStrategy.setLockTimeout( timeout, jdbcConnection, factory ); } - } - - private static class LockTimeoutResetter implements JdbcAction { - private final ConnectionLockTimeoutStrategy lockTimeoutStrategy; - private final LockTimeoutSetter setter; - - public LockTimeoutResetter(ConnectionLockTimeoutStrategy lockTimeoutStrategy, LockTimeoutSetter setter) { - this.lockTimeoutStrategy = lockTimeoutStrategy; - this.setter = setter; - } @Override - public void perform(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + public void performPostAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); // reset the timeout - lockTimeoutStrategy.setLockTimeout( setter.getBaseline(), jdbcConnection, factory ); + lockTimeoutStrategy.setLockTimeout( baseline, jdbcConnection, factory ); } } From 1584943f5d7152de9e06cffa80953206613598c1 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 13 Aug 2025 15:30:55 -0500 Subject: [PATCH 3/3] HHH-19602 - Adjust JdbcOperation to allow more-than-one statement --- .../sql/exec/internal/AbstractDatabaseOperation.java | 12 +++++++++--- .../exec/internal/DatabaseOperationSelectImpl.java | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java index a727d4a341aa..9ceab8636c6a 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java @@ -12,7 +12,6 @@ import org.hibernate.sql.exec.spi.SecondaryAction; import org.hibernate.sql.exec.spi.StatementAccess; -import java.lang.reflect.Array; import java.sql.Connection; import java.util.ArrayList; import java.util.Collections; @@ -57,11 +56,18 @@ protected void performPostActions( } } - protected static T[] toArray(Class type, List actions) { + protected static PreAction[] toPreActionArray(List actions) { if ( CollectionHelper.isEmpty( actions ) ) { return null; } - return actions.toArray( (T[]) Array.newInstance( type, 0 ) ); + return actions.toArray( new PreAction[0] ); + } + + protected static PostAction[] toPostActionArray(List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + return actions.toArray( new PostAction[0] ); } protected abstract static class Builder> { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java index a2bf683073de..56222cdb883c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java @@ -146,8 +146,8 @@ public DatabaseOperationSelectImpl build() { if ( preActions == null && postActions == null ) { return new DatabaseOperationSelectImpl( primaryAction ); } - final PreAction[] preActions = toArray( PreAction.class, this.preActions ); - final PostAction[] postActions = toArray( PostAction.class, this.postActions ); + final PreAction[] preActions = toPreActionArray( this.preActions ); + final PostAction[] postActions = toPostActionArray( this.postActions ); return new DatabaseOperationSelectImpl( preActions, postActions, primaryAction ); } }