diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java index 086fedac35..1d4a5c08de 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java @@ -18,6 +18,7 @@ import java.util.function.Supplier; import org.hibernate.ConnectionReleaseMode; import org.hibernate.HibernateException; import org.hibernate.TransactionException; +import org.hibernate.engine.jdbc.batch.JdbcBatchLogging; import org.hibernate.engine.jdbc.batch.spi.Batch; import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementGroup; @@ -192,6 +193,18 @@ public class JdbcCoordinatorImpl implements JdbcCoordinator { } } + @Override + public void conditionallyExecuteBatch(BatchKey key) { + if ( currentBatch == null ) { + return; + } + + if ( !currentBatch.getKey().equals( key ) ) { + JdbcBatchLogging.BATCH_LOGGER.debugf( "Conditionally executing batch - %s", currentBatch.getKey() ); + currentBatch.execute(); + } + } + @Override public void abortBatch() { if ( currentBatch != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java index 48c58beac5..c5ff883936 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java @@ -8,6 +8,7 @@ package org.hibernate.engine.jdbc.mutation.internal; import java.sql.SQLException; +import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.MutationExecutor; import org.hibernate.engine.jdbc.mutation.OperationResultChecker; @@ -26,6 +27,15 @@ import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER * @author Steve Ebersole */ public abstract class AbstractMutationExecutor implements MutationExecutor { + /** + * Executors with non-batched operations should call this to clean up any "previous" batch + * before starting their work + */ + protected void prepareForNonBatchedWork(BatchKey batchKey, SharedSessionContractImplementor session) { + // if there is a current batch, make sure to execute it first + session.getJdbcCoordinator().conditionallyExecuteBatch( batchKey ); + } + /** * Templated implementation of execution as
    *
  1. {@link #performNonBatchedOperations}
  2. diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleNonBatched.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleNonBatched.java index b302fdc96f..5c553a0f60 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleNonBatched.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleNonBatched.java @@ -22,8 +22,8 @@ public class MutationExecutorSingleNonBatched extends AbstractSingleMutationExec PreparableMutationOperation mutationOperation, SharedSessionContractImplementor session) { super( mutationOperation, session ); - this.statementGroup = new PreparedStatementGroupSingleTable( mutationOperation, session ); + prepareForNonBatchedWork( null, session ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleSelfExecuting.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleSelfExecuting.java index f1f5adcfa7..3fa4523114 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleSelfExecuting.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorSingleSelfExecuting.java @@ -33,6 +33,8 @@ public class MutationExecutorSingleSelfExecuting extends AbstractMutationExecuto this::findJdbcValueDescriptor, session ); + + prepareForNonBatchedWork( null, session ); } private JdbcValueDescriptor findJdbcValueDescriptor(String tableName, String columnName, ParameterUsage usage) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorStandard.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorStandard.java index 227bb5b0fc..3ebb7067a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/MutationExecutorStandard.java @@ -21,7 +21,9 @@ import org.hibernate.engine.jdbc.mutation.ParameterUsage; import org.hibernate.engine.jdbc.mutation.TableInclusionChecker; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementGroup; +import org.hibernate.engine.jdbc.mutation.spi.BatchKeyAccess; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; import org.hibernate.sql.model.PreparableMutationOperation; @@ -30,6 +32,8 @@ import org.hibernate.sql.model.TableMapping; import org.hibernate.sql.model.ValuesAnalysis; import org.hibernate.sql.model.jdbc.JdbcValueDescriptor; +import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; + /** * Standard MutationExecutor implementation * @@ -60,12 +64,12 @@ public class MutationExecutorStandard extends AbstractMutationExecutor { public MutationExecutorStandard( MutationOperationGroup mutationOperationGroup, - Supplier batchKeySupplier, + BatchKeyAccess batchKeySupplier, int batchSize, SharedSessionContractImplementor session) { this.mutationOperationGroup = mutationOperationGroup; - final BatchKey batchKey = batchKeySupplier.get(); + final BatchKey batchKey = batchKeySupplier.getBatchKey(); // split the table operations into batchable and non-batchable - // 1. batchable statements are handle via Batch @@ -155,6 +159,10 @@ public class MutationExecutorStandard extends AbstractMutationExecutor { this::findJdbcValueDescriptor, session ); + + if ( isNotEmpty( nonBatchedJdbcMutations ) || isNotEmpty( selfExecutingMutations ) ) { + prepareForNonBatchedWork( batchKey, session ); + } } protected PreparedStatementGroup getNonBatchedStatementGroup() { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/NoBatchKeyAccess.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/NoBatchKeyAccess.java new file mode 100644 index 0000000000..a6c8dde6d3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/NoBatchKeyAccess.java @@ -0,0 +1,28 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.engine.jdbc.mutation.internal; + +import org.hibernate.engine.jdbc.batch.spi.BatchKey; +import org.hibernate.engine.jdbc.mutation.spi.BatchKeyAccess; + +/** + * A form of BatchKeyAccess for cases where batching is not wanted, which is + * signified by a BatchKey of {@code null} + * + * @author Steve Ebersole + */ +public class NoBatchKeyAccess implements BatchKeyAccess { + /** + * Singleton access + */ + public static final NoBatchKeyAccess INSTANCE = new NoBatchKeyAccess(); + + @Override + public BatchKey getBatchKey() { + return null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/StandardMutationExecutorService.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/StandardMutationExecutorService.java index 4dcbedfcb4..0a0bba1192 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/StandardMutationExecutorService.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/StandardMutationExecutorService.java @@ -12,6 +12,7 @@ import java.util.function.Supplier; import org.hibernate.cfg.Environment; import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.jdbc.mutation.spi.BatchKeyAccess; import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.config.ConfigurationHelper; @@ -44,7 +45,7 @@ public class StandardMutationExecutorService implements MutationExecutorService @Override public MutationExecutor createExecutor( - Supplier batchKeySupplier, + BatchKeyAccess batchKeySupplier, MutationOperationGroup operationGroup, SharedSessionContractImplementor session) { // decide whether to use batching - any number > one means to batch @@ -78,7 +79,7 @@ public class StandardMutationExecutorService implements MutationExecutorService } final PreparableMutationOperation jdbcOperation = (PreparableMutationOperation) singleOperation; - final BatchKey batchKey = batchKeySupplier.get(); + final BatchKey batchKey = batchKeySupplier.getBatchKey(); if ( jdbcOperation.canBeBatched( batchKey, batchSizeToUse ) ) { return new MutationExecutorSingleBatched( jdbcOperation, batchKey, batchSizeToUse, session ); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/BatchKeyAccess.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/BatchKeyAccess.java new file mode 100644 index 0000000000..0821058745 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/BatchKeyAccess.java @@ -0,0 +1,21 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.engine.jdbc.mutation.spi; + +import org.hibernate.engine.jdbc.batch.spi.BatchKey; + +/** + * Provides access to a BatchKey as part of creating an {@linkplain MutationExecutorService#createExecutor executor}. + * + * @author Steve Ebersole + */ +public interface BatchKeyAccess { + /** + * The BatchKey to use + */ + BatchKey getBatchKey(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/MutationExecutorService.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/MutationExecutorService.java index b7d2a443a6..a76222a2aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/MutationExecutorService.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/spi/MutationExecutorService.java @@ -6,9 +6,6 @@ */ package org.hibernate.engine.jdbc.mutation.spi; -import java.util.function.Supplier; - -import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.MutationExecutor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.service.Service; @@ -21,8 +18,11 @@ import org.hibernate.sql.model.MutationOperationGroup; */ public interface MutationExecutorService extends Service { + /** + * Create an executor for the given {@code operationGroup}, potentially using batching + */ MutationExecutor createExecutor( - Supplier batchKeySupplier, + BatchKeyAccess batchKeySupplier, MutationOperationGroup operationGroup, SharedSessionContractImplementor session); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java index 757f88faa4..42fd973cf0 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java @@ -60,6 +60,12 @@ public interface JdbcCoordinator extends Serializable, TransactionCoordinatorOwn */ void executeBatch(); + /** + * Conditionally execute the currently managed batch (if any), if the + * keys do not match + */ + void conditionallyExecuteBatch(BatchKey key); + /** * Abort the currently managed batch (if any) */ diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java index 378b6b5ff3..fce4d1d4e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java @@ -10,8 +10,11 @@ import java.util.List; import org.hibernate.Internal; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.internal.NoBatchKeyAccess; +import org.hibernate.engine.jdbc.mutation.spi.BatchKeyAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.OnExecutionGenerator; @@ -60,6 +63,18 @@ public abstract class AbstractMutationCoordinator { return factory().getJdbcServices().getDialect(); } + protected BatchKeyAccess resolveBatchKeyAccess(boolean dynamicUpdate, SharedSessionContractImplementor session) { + if ( !dynamicUpdate + && session.getTransactionCoordinator() != null + && session.getTransactionCoordinator().isTransactionActive() ) { + return this::getBatchKey; + } + + return NoBatchKeyAccess.INSTANCE; + } + + protected abstract BatchKey getBatchKey(); + protected MutationOperationGroup createOperationGroup(ValuesAnalysis valuesAnalysis, MutationGroup mutationGroup) { final int numberOfTableMutations = mutationGroup.getNumberOfTableMutations(); switch ( numberOfTableMutations ) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java index 3e96f7d060..2e35b0591c 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java @@ -133,9 +133,7 @@ public class DeleteCoordinator extends AbstractMutationCoordinator { return session.getFactory() .getServiceRegistry() .getService( MutationExecutorService.class ) - .createExecutor( ( session.getTransactionCoordinator() != null && - session.getTransactionCoordinator().isTransactionActive() ? () -> batchKey : () -> null ), - group, session ); + .createExecutor( resolveBatchKeyAccess( false, session ), group, session ); } protected void applyLocking( diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java index 2631a1a4c9..dc25828cd0 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java @@ -12,6 +12,7 @@ import java.util.List; import org.hibernate.Internal; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.MutationExecutor; import org.hibernate.engine.jdbc.mutation.ParameterUsage; @@ -79,7 +80,8 @@ public class InsertCoordinator extends AbstractMutationCoordinator { return staticInsertGroup; } - public BasicBatchKey getInsertBatchKey() { + @Override + protected BatchKey getBatchKey() { return batchKey; } @@ -305,9 +307,7 @@ public class InsertCoordinator extends AbstractMutationCoordinator { return session.getFactory() .getServiceRegistry() .getService( MutationExecutorService.class ) - .createExecutor( ( !dynamicUpdate && session.getTransactionCoordinator() != null && - session.getTransactionCoordinator().isTransactionActive() ? () -> batchKey : () -> null ), - group, session ); + .createExecutor( resolveBatchKeyAccess( dynamicUpdate, session ), group, session ); } protected static TableInclusionChecker getTableInclusionChecker(InsertValuesAnalysis insertValuesAnalysis) { @@ -410,4 +410,12 @@ public class InsertCoordinator extends AbstractMutationCoordinator { && generator.generatedOnExecution() && ( (OnExecutionGenerator) generator ).referenceColumnsInSql(dialect); } + + /** + * @deprecated Use {@link #getBatchKey()} + */ + @Deprecated + public BasicBatchKey getInsertBatchKey() { + return batchKey; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java index 42223e4995..8b5d166157 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java @@ -970,9 +970,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple return session.getSessionFactory() .getServiceRegistry() .getService( MutationExecutorService.class ) - .createExecutor( ( !dynamicUpdate && session.getTransactionCoordinator() != null && - session.getTransactionCoordinator().isTransactionActive() ? () -> batchKey : () -> null ), - group, session ); + .createExecutor( resolveBatchKeyAccess( dynamicUpdate, session ), group, session ); } protected MutationOperationGroup generateDynamicUpdateGroup( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchGeneratedAssociationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchGeneratedAssociationTest.java new file mode 100644 index 0000000000..691bdc3791 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchGeneratedAssociationTest.java @@ -0,0 +1,109 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.batch; + +import java.io.Serializable; + +import org.hibernate.annotations.Generated; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +@SessionFactory +@DomainModel( annotatedClasses = { + BatchGeneratedAssociationTest.Interpretation.class, + BatchGeneratedAssociationTest.InterpretationData.class, + BatchGeneratedAssociationTest.InterpretationVersion.class +} ) +@ServiceRegistry( settings = @Setting( name = AvailableSettings.STATEMENT_BATCH_SIZE, value = "10" ) ) +public class BatchGeneratedAssociationTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long interpretationVersion = 111L; + + final Interpretation interpretation = new Interpretation(); + interpretation.uuid = 1L; + + final InterpretationData interpretationData = new InterpretationData(); + interpretationData.interpretationVersion = new InterpretationVersion( + interpretationVersion, + interpretation.uuid + ); + interpretationData.name = "TEST_NAME"; + session.persist( interpretationData ); + + interpretation.interpretationData = interpretationData; + interpretation.interpretationVersion = interpretationVersion; + session.persist( interpretation ); + } ); + } + + @Entity( name = "Interpretation" ) + @Table( name = "interpretations" ) + public static class Interpretation { + @Id + public Long uuid; + + @Column( name = "interpretation_version" ) + public Long interpretationVersion; + + @Column( name = "id" ) + @Generated + public Long id; + + @OneToOne( fetch = FetchType.LAZY ) + @JoinColumns( { + @JoinColumn( name = "uuid", referencedColumnName = "interpretation_uuid", insertable = false, updatable = false ), + @JoinColumn( name = "interpretation_version", referencedColumnName = "interpretation_version", insertable = false, updatable = false ) + } ) + public InterpretationData interpretationData; + } + + @Embeddable + public static class InterpretationVersion implements Serializable { + @Column( name = "interpretation_version", nullable = false, updatable = false ) + public Long version; + + @Column( name = "interpretation_uuid", nullable = false, updatable = false ) + public Long uuid; + + public InterpretationVersion() { + } + + public InterpretationVersion(Long version, Long uuid) { + this.version = version; + this.uuid = uuid; + } + } + + @Entity( name = "InterpretationData" ) + @Table( name = "interpretation_data" ) + public static class InterpretationData { + @EmbeddedId + public InterpretationVersion interpretationVersion; + + @Column( updatable = false ) + public String name; + } +}