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..9ceab8636c6a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java @@ -0,0 +1,162 @@ +/* + * 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 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.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 PreAction[] preActions; + protected final PostAction[] postActions; + + public AbstractDatabaseOperation() { + this( null, null ); + } + + public AbstractDatabaseOperation(PreAction[] preActions, PostAction[] postActions) { + this.preActions = preActions; + this.postActions = postActions; + } + + protected void performPreActions( + StatementAccess statementAccess, + Connection jdbcConnection, + ExecutionContext 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) { + if ( postActions != null ) { + for ( int i = 0; i < postActions.length; i++ ) { + postActions[i].performPostAction( statementAccess, jdbcConnection, executionContext ); + } + } + } + + protected static PreAction[] toPreActionArray(List actions) { + if ( CollectionHelper.isEmpty( actions ) ) { + return null; + } + 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> { + protected List preActions; + protected List postActions; + + 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(); + } + + /** + * 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(); + } + } +} 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..56222cdb883c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java @@ -0,0 +1,154 @@ +/* + * 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.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.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.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( + PreAction[] preActions, + PostAction[] 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 StatementAccessImpl statementAccess = new StatementAccessImpl( + 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 extends AbstractDatabaseOperation.Builder { + private final JdbcOperationQuerySelect primaryAction; + + private Builder(JdbcOperationQuerySelect primaryAction) { + this.primaryAction = primaryAction; + } + + @Override + protected Builder getThis() { + return this; + } + + public DatabaseOperationSelectImpl build() { + if ( preActions == null && postActions == null ) { + return new DatabaseOperationSelectImpl( primaryAction ); + } + final PreAction[] preActions = toPreActionArray( this.preActions ); + final PostAction[] postActions = toPostActionArray( 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..de370033df26 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java @@ -0,0 +1,33 @@ +/* + * 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 org.hibernate.sql.exec.spi.StatementAccess; + +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/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/DatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java new file mode 100644 index 000000000000..60219e24a015 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.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.util.Set; + +/** + * 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. + */ + 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..32a404c1b220 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java @@ -0,0 +1,33 @@ +/* + * 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.PreparedStatement; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * {@linkplain DatabaseOperation} whose primary operation is a mutation. + * + * @author Steve Ebersole + */ +@Incubating +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..66e3f2ee76ef --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +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; + +import java.sql.PreparedStatement; + +/** + * {@linkplain DatabaseOperation} whose primary operation is a selection. + * + * @author Steve Ebersole + */ +@Incubating +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/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 new file mode 100644 index 000000000000..e9a8630acf43 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java @@ -0,0 +1,278 @@ +/* + * 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.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; +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 LockTimeoutHandler lockTimeoutHandler = new LockTimeoutHandler( Timeout.seconds( 2 ), lockTimeoutStrategy ); + + final DatabaseOperationSelectImpl databaseOperation = DatabaseOperationSelectImpl.builder( jdbcOperation ) + .addSecondaryActionPair( lockTimeoutHandler ) + .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 LockTimeoutHandler implements PreAction, PostAction { + private final ConnectionLockTimeoutStrategy lockTimeoutStrategy; + private final Timeout timeout; + private Timeout baseline; + + 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 ); + } + + @Override + public void performPostAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + // reset the timeout + lockTimeoutStrategy.setLockTimeout( baseline, 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; + } + + } +}