From a372c3aa340d16c516dcab017d53a55583f99c84 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 25 Sep 2025 15:10:28 -0600 Subject: [PATCH 1/4] HHH-19774 - Automatic flushing for child session with shared connection/tx HHH-19808 - Automatic closing for child session with shared connection/tx --- .../internal/ParentSessionCallbacks.java | 22 ++ .../SessionCreationOptionsAdaptor.java | 19 +- .../internal/SharedSessionBuilderImpl.java | 16 ++ .../SharedSessionCreationOptions.java | 6 + .../AbstractSharedSessionContract.java | 26 ++- .../org/hibernate/internal/SessionImpl.java | 45 +++- .../hibernate/internal/SessionLogging.java | 9 + .../internal/StatelessSessionImpl.java | 43 +++- .../AbstractStatefulStatelessFilterTest.java | 13 +- .../joined2/JoinedInheritanceFilterTest.java | 3 +- .../SimpleSharedSessionBuildingTests.java | 208 ++++++++++++++++-- .../orm/junit/SessionFactoryExtension.java | 31 ++- .../orm/junit/SessionFactoryScope.java | 8 +- .../orm/transaction/TransactionUtil.java | 4 +- 14 files changed, 389 insertions(+), 64 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java new file mode 100644 index 000000000000..19ead8ff2500 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.engine.creation.internal; + +/** + * Callbacks from a parent session to a child session for certain events. + * + * @author Steve Ebersole + */ +public interface ParentSessionCallbacks { + /** + * Callback when the parent is flushed. + */ + void onParentFlush(); + + /** + * Callback when the parent is closed. + */ + void onParentClose(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java index 491755c0bd71..e420255ef70a 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java @@ -12,6 +12,7 @@ import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.TransactionCompletionCallbacksImplementor; +import org.hibernate.internal.AbstractSharedSessionContract; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.resource.transaction.spi.TransactionCoordinator; @@ -31,7 +32,8 @@ */ public record SessionCreationOptionsAdaptor( SessionFactoryImplementor factory, - CommonSharedSessionCreationOptions options) + CommonSharedSessionCreationOptions options, + AbstractSharedSessionContract originalSession) implements SharedSessionCreationOptions { @Override @@ -143,4 +145,19 @@ public Transaction getTransaction() { public TransactionCompletionCallbacksImplementor getTransactionCompletionCallbacks() { return options.getTransactionCompletionCallbacksImplementor(); } + + @Override + public void registerParentSessionCallbacks(ParentSessionCallbacks callbacks) { + originalSession.getEventListenerManager().addListener( new SessionEventListener() { + @Override + public void flushStart() { + callbacks.onParentFlush(); + } + + @Override + public void end() { + callbacks.onParentClose(); + } + } ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java index 7b0fee5308e2..83c5b45896dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java @@ -259,6 +259,22 @@ public SharedSessionBuilderImplementor subselectFetchEnabled(boolean subselectFe // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SharedSessionCreationOptions + + @Override + public void registerParentSessionCallbacks(ParentSessionCallbacks callbacks) { + original.getEventListenerManager().addListener( new SessionEventListener() { + @Override + public void flushStart() { + callbacks.onParentFlush(); + } + + @Override + public void end() { + callbacks.onParentClose(); + } + } ); + } + @Override public boolean isTransactionCoordinatorShared() { return shareTransactionContext; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java index 4cfd6ad693f8..c36736682835 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java @@ -26,4 +26,10 @@ public interface SharedSessionCreationOptions extends SessionCreationOptions { JdbcCoordinator getJdbcCoordinator(); Transaction getTransaction(); TransactionCompletionCallbacksImplementor getTransactionCompletionCallbacks(); + + /** + * Registers callbacks for the child session to integrate with events of the parent session. + */ + void registerParentSessionCallbacks(ParentSessionCallbacks callbacks); + } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 822b287dc180..c1a78930a96d 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -10,7 +10,6 @@ import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; - import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.EntityNameResolver; @@ -28,6 +27,7 @@ import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.creation.internal.ParentSessionCallbacks; import org.hibernate.engine.creation.internal.SessionCreationOptions; import org.hibernate.engine.creation.internal.SessionCreationOptionsAdaptor; import org.hibernate.engine.creation.internal.SharedSessionBuilderImpl; @@ -225,6 +225,17 @@ public AbstractSharedSessionContract(SessionFactoryImpl factory, SessionCreation jdbcSessionContext = createJdbcSessionContext( statementInspector ); logInconsistentOptions( sharedOptions ); addSharedSessionTransactionObserver( transactionCoordinator ); + sharedOptions.registerParentSessionCallbacks( new ParentSessionCallbacks() { + @Override + public void onParentFlush() { + propagateFlush(); + } + + @Override + public void onParentClose() { + propagateClose(); + } + } ); } else { isTransactionCoordinatorShared = false; @@ -249,7 +260,7 @@ public SharedStatelessSessionBuilder statelessWithOptions() { @Override protected StatelessSessionImplementor createStatelessSession() { return new StatelessSessionImpl( factory, - new SessionCreationOptionsAdaptor( factory, this ) ); + new SessionCreationOptionsAdaptor( factory, this, AbstractSharedSessionContract.this ) ); } }; } @@ -494,6 +505,13 @@ public boolean isClosed() { @Override public void close() { + if ( isTransactionCoordinatorShared ) { + // Perform an auto-flush - + // This deals with the natural usage pattern of a child Session + // used with a try-with-resource block + propagateFlush(); + } + if ( !closed || waitingForAutoClose ) { try { delayedAfterCompletion(); @@ -538,6 +556,10 @@ protected void setClosed() { cleanupOnClose(); } + protected abstract void propagateFlush(); + + protected abstract void propagateClose(); + protected void checkBeforeClosingJdbcCoordinator() { } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index dfa9b3816877..7d514add8a18 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -196,7 +196,7 @@ public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { identifierRollbackEnabled = options.isIdentifierRollbackEnabled(); - setUpTransactionCompletionProcesses( options, actionQueue ); + setUpTransactionCompletionProcesses( options, actionQueue, this ); loadQueryInfluencers = new LoadQueryInfluencers( factory, options ); @@ -224,12 +224,23 @@ public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { } } - private static void setUpTransactionCompletionProcesses(SessionCreationOptions options, ActionQueue actionQueue) { + private static void setUpTransactionCompletionProcesses( + SessionCreationOptions options, + ActionQueue actionQueue, + SessionImpl childSession) { if ( options instanceof SharedSessionCreationOptions sharedOptions && sharedOptions.isTransactionCoordinatorShared() ) { final var callbacks = sharedOptions.getTransactionCompletionCallbacks(); if ( callbacks != null ) { actionQueue.setTransactionCompletionCallbacks( callbacks, true ); +// // register a callback with the child session to propagate auto flushing +// callbacks.registerCallback( session -> { +// // NOTE: `session` here is the parent +// assert session != childSession; +// if ( !childSession.isClosed() && childSession.getHibernateFlushMode() != FlushMode.MANUAL ) { +// childSession.triggerChildAutoFlush(); +// } +// } ); } } } @@ -1424,6 +1435,36 @@ private void fireFlush() { } } + /** + * Used for auto flushing shared/child session as part of the parent session's auto flush. + */ + @Override + public void propagateFlush() { + if ( isClosed() ) { + return; + } + if ( !isReadOnly() ) { + try { + SESSION_LOGGER.automaticallyFlushingChildSession(); + eventListenerGroups.eventListenerGroup_FLUSH + .fireEventOnEachListener( new FlushEvent( this ), + FlushEventListener::onFlush ); + } + catch (RuntimeException e) { + throw getExceptionConverter().convert( e ); + } + } + } + + @Override + public void propagateClose() { + if ( isClosed() ) { + return; + } + SESSION_LOGGER.automaticallyClosingChildSession(); + closeWithoutOpenChecks(); + } + @Override public void setHibernateFlushMode(FlushMode flushMode) { this.flushMode = flushMode; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java index 813f184a9635..cddf971d6999 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java @@ -76,10 +76,19 @@ public interface SessionLogging extends BasicLogger { @Message("Automatically flushing session") void automaticallyFlushingSession(); + @LogMessage(level = TRACE) + @Message("Automatically flushing child session") + void automaticallyFlushingChildSession(); + @LogMessage(level = TRACE) @Message("Automatically closing session") void automaticallyClosingSession(); + + @LogMessage(level = TRACE) + @Message("Automatically closing child session") + void automaticallyClosingChildSession(); + @LogMessage(level = TRACE) @Message("%s remove orphan before updates: [%s]") void removeOrphanBeforeUpdates(String timing, String entityInfo); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index 515ba5fb7c0f..eee651170480 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -121,16 +121,28 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen private final boolean connectionProvided; private final TransactionCompletionCallbacksImplementor transactionCompletionCallbacks; + private final FlushMode flushMode; private final EventListenerGroups eventListenerGroups; public StatelessSessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { super( factory, options ); connectionProvided = options.getConnection() != null; - transactionCompletionCallbacks = - options instanceof SharedSessionCreationOptions sharedOptions - && sharedOptions.isTransactionCoordinatorShared() - ? sharedOptions.getTransactionCompletionCallbacks() - : new TransactionCompletionCallbacksImpl( this ); + if ( options instanceof SharedSessionCreationOptions sharedOptions + && sharedOptions.isTransactionCoordinatorShared() ) { + transactionCompletionCallbacks = sharedOptions.getTransactionCompletionCallbacks(); +// // register a callback with the child session to propagate auto flushing +// transactionCompletionCallbacks.registerCallback( session -> { +// // NOTE: `session` here is the parent +// if ( !isClosed() ) { +// triggerChildAutoFlush(); +// } +// } ); + flushMode = FlushMode.AUTO; + } + else { + transactionCompletionCallbacks = new TransactionCompletionCallbacksImpl( this ); + flushMode = FlushMode.MANUAL; + } temporaryPersistenceContext = createPersistenceContext( this ); influencers = new LoadQueryInfluencers( getFactory() ); eventListenerGroups = factory.getEventListenerGroups(); @@ -147,7 +159,8 @@ public boolean shouldAutoJoinTransaction() { @Override public FlushMode getHibernateFlushMode() { - return FlushMode.MANUAL; + // NOTE: only ever *not* MANUAL when this is a "child session" + return flushMode; } private StatisticsImplementor getStatistics() { @@ -1199,6 +1212,24 @@ private void managedFlush() { getJdbcCoordinator().executeBatch(); } + @Override + public void propagateFlush() { + if ( isClosed() ) { + return; + } + SESSION_LOGGER.automaticallyFlushingChildSession(); + getJdbcCoordinator().executeBatch(); + } + + @Override + protected void propagateClose() { + if ( isClosed() ) { + return; + } + SESSION_LOGGER.automaticallyClosingChildSession(); + close(); + } + @Override public String bestGuessEntityName(Object object) { final var lazyInitializer = extractLazyInitializer( object ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/AbstractStatefulStatelessFilterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/AbstractStatefulStatelessFilterTest.java index 79f9f1c4a671..f94b645f1184 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/AbstractStatefulStatelessFilterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/AbstractStatefulStatelessFilterTest.java @@ -4,18 +4,17 @@ */ package org.hibernate.orm.test.filter; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import org.hibernate.StatelessSession; import org.hibernate.engine.spi.SessionImplementor; - +import org.hibernate.engine.spi.StatelessSessionImplementor; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; import org.junit.jupiter.params.provider.Arguments; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + @SessionFactory public abstract class AbstractStatefulStatelessFilterTest implements SessionFactoryScopeAware { @@ -29,7 +28,7 @@ public void injectSessionFactoryScope(SessionFactoryScope scope) { protected List transactionKind() { // We want to test both regular and stateless session: BiConsumer> kind1 = SessionFactoryScope::inTransaction; - BiConsumer> kind2 = SessionFactoryScope::inStatelessTransaction; + BiConsumer> kind2 = SessionFactoryScope::inStatelessTransaction; return List.of( Arguments.of( kind1 ), Arguments.of( kind2 ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/subclass/joined2/JoinedInheritanceFilterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/subclass/joined2/JoinedInheritanceFilterTest.java index 1e659e4d3a1a..c7aac1ad69ac 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/subclass/joined2/JoinedInheritanceFilterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/subclass/joined2/JoinedInheritanceFilterTest.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.ParamDef; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.StatelessSessionImplementor; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; @@ -61,7 +62,7 @@ List transactionKind() { // We want to test both regular and stateless session: BiConsumer> kind1 = SessionFactoryScope::inTransaction; TriFunction, Object, Object> find1 = Session::get; - BiConsumer> kind2 = SessionFactoryScope::inStatelessTransaction; + BiConsumer> kind2 = SessionFactoryScope::inStatelessTransaction; TriFunction, Object, Object> find2 = StatelessSession::get; return List.of( Arguments.of( kind1, find1 ), diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java index 0e8977014739..84f74e9083aa 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java @@ -7,9 +7,13 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.hibernate.FlushMode; import org.hibernate.Interceptor; +import org.hibernate.SessionEventListener; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.StatelessSessionImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; @@ -18,7 +22,6 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertSame; /** * @author Steve Ebersole @@ -105,44 +108,190 @@ void testInterceptor(SessionFactoryScope factoryScope) { } @Test - void testUsage(SessionFactoryScope factoryScope) { - final var sqlCollector = factoryScope.getCollectingStatementInspector(); + void testConnectionAndTransactionSharing(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (parentSession) -> { + assertThat( parentSession.getHibernateFlushMode() ).isEqualTo( FlushMode.AUTO ); + + // child stateful session + try (SessionImplementor childSession = (SessionImplementor) parentSession + .sessionWithOptions() + .connection() + .openSession()) { + assertThat( childSession.getHibernateFlushMode() ) + .isEqualTo( FlushMode.AUTO ); + assertThat( childSession.getTransaction() ) + .isSameAs( parentSession.getTransaction() ); + assertThat( childSession.getJdbcCoordinator() ) + .isSameAs( parentSession.getJdbcCoordinator() ); + assertThat( childSession.getTransactionCompletionCallbacksImplementor() ) + .isSameAs( parentSession.getTransactionCompletionCallbacksImplementor() ); + } + + // child stateless session + try (StatelessSessionImplementor childSession = (StatelessSessionImplementor) parentSession + .statelessWithOptions() + .connection() + .open()) { + assertThat( childSession.getHibernateFlushMode() ) + .isEqualTo( FlushMode.AUTO ); + assertThat( childSession.getTransaction() ) + .isSameAs( parentSession.getTransaction() ); + assertThat( childSession.getJdbcCoordinator() ) + .isSameAs( parentSession.getJdbcCoordinator() ); + assertThat( childSession.getTransactionCompletionCallbacksImplementor() ) + .isSameAs( parentSession.getTransactionCompletionCallbacksImplementor() ); + } + } ); + } + + @Test + void testClosePropagation(SessionFactoryScope factoryScope) { + final MutableObject parentSessionRef = new MutableObject<>(); + final MutableObject childSessionRef = new MutableObject<>(); + + factoryScope.inTransaction( (parentSession) -> { + parentSessionRef.set( parentSession ); + + var childSession = (SessionImplementor) parentSession + .sessionWithOptions() + .connection() + .openSession(); + childSessionRef.set( childSession ); + } ); + + assertThat( parentSessionRef.get().isClosed() ).isTrue(); + assertThat( childSessionRef.get().isClosed() ).isTrue(); - // try from Session + parentSessionRef.set( null ); + childSessionRef.set( null ); + + factoryScope.inTransaction( (parentSession) -> { + parentSessionRef.set( parentSession ); + + var childSession = (StatelessSessionImplementor) parentSession + .statelessWithOptions() + .connection() + .open(); + childSessionRef.set( childSession ); + } ); + + assertThat( parentSessionRef.get().isClosed() ).isTrue(); + assertThat( childSessionRef.get().isClosed() ).isTrue(); + } + + /** + * NOTE: builds on assertions from {@link #testConnectionAndTransactionSharing} + */ + @Test + void testAutoFlushStatefulChild(SessionFactoryScope factoryScope) { + final var sqlCollector = factoryScope.getCollectingStatementInspector(); sqlCollector.clear(); - factoryScope.inTransaction( (statefulSession) -> { - try (var session = statefulSession + + factoryScope.inTransaction( (parentSession) -> { + try (SessionImplementor childSession = (SessionImplementor) parentSession .sessionWithOptions() .connection() .openSession()) { - session.persist( new Something( 1, "first" ) ); - assertSame( statefulSession.getTransaction(), session.getTransaction() ); - assertSame( statefulSession.getJdbcCoordinator(), - ((SessionImplementor) session).getJdbcCoordinator() ); - assertSame( statefulSession.getTransactionCompletionCallbacksImplementor(), - ((SessionImplementor) session).getTransactionCompletionCallbacksImplementor() ); - session.flush(); //TODO: should not be needed! + // persist an entity through the child session - + // should be auto flushed (technically as part of the try-with-resources close of the child session) + childSession.persist( new Something( 1, "first" ) ); } } ); + + // make sure the flush and insert happened assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + factoryScope.inTransaction( (session) -> { + final Something created = session.find( Something.class, 1 ); + assertThat( created ).isNotNull(); + } ); + + } - // try from StatelessSession + /** + * NOTE: builds on assertions from {@link #testConnectionAndTransactionSharing} + * and {@linkplain #testClosePropagation} + */ + @Test + void testAutoFlushStatefulChildNoClose(SessionFactoryScope factoryScope) { + final var sqlCollector = factoryScope.getCollectingStatementInspector(); sqlCollector.clear(); - factoryScope.inStatelessTransaction( (statelessSession) -> { - try (var statefulSession = statelessSession + + factoryScope.inTransaction( (parentSession) -> { + SessionImplementor childSession = (SessionImplementor) parentSession + .sessionWithOptions() + .connection() + .openSession(); + + // persist an entity through the shared/child session. + // then make sure the auto-flush of the parent session + // propagates to the shared/child + childSession.persist( new Something( 1, "first" ) ); + } ); + + // make sure the flush and insert happened + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + factoryScope.inTransaction( (session) -> { + final Something created = session.find( Something.class, 1 ); + assertThat( created ).isNotNull(); + } ); + } + + + + /** + * NOTE: builds on assertions from {@link #testConnectionAndTransactionSharing} + */ + @Test + void testAutoFlushStatelessChild(SessionFactoryScope factoryScope) { + final var sqlCollector = factoryScope.getCollectingStatementInspector(); + sqlCollector.clear(); + + factoryScope.inStatelessTransaction( (parentSession) -> { + try (SessionImplementor childSession = (SessionImplementor) parentSession .sessionWithOptions() .connection() .openSession()) { - statefulSession.persist( new Something( 2, "first" ) ); - assertSame( statefulSession.getTransaction(), statelessSession.getTransaction() ); - assertSame( ((SessionImplementor) statefulSession).getJdbcCoordinator(), - ((StatelessSessionImplementor) statelessSession).getJdbcCoordinator() ); - assertSame( ((SessionImplementor) statefulSession).getTransactionCompletionCallbacksImplementor(), - ((StatelessSessionImplementor) statelessSession).getTransactionCompletionCallbacksImplementor() ); - statefulSession.flush(); //TODO: should not be needed! + // persist an entity through the child session - + // should be auto flushed (technically as part of the try-with-resources close of the child session) + childSession.persist( new Something( 1, "first" ) ); } } ); + + // make sure the flush and insert happened + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + factoryScope.inTransaction( (session) -> { + final Something created = session.find( Something.class, 1 ); + assertThat( created ).isNotNull(); + } ); + } + + /** + * NOTE: builds on assertions from {@link #testConnectionAndTransactionSharing} + * and {@linkplain #testClosePropagation} + */ + @Test + void testAutoFlushStatelessChildNoClose(SessionFactoryScope factoryScope) { + final var sqlCollector = factoryScope.getCollectingStatementInspector(); + sqlCollector.clear(); + + factoryScope.inStatelessTransaction( (parentSession) -> { + SessionImplementor childSession = (SessionImplementor) parentSession + .sessionWithOptions() + .connection() + .openSession(); + + // persist an entity through the shared/child session. + // then make sure the auto-flush of the parent session + // propagates to the shared/child + childSession.persist( new Something( 1, "first" ) ); + } ); + + // make sure the flush and insert happened assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + factoryScope.inTransaction( (session) -> { + final Something created = session.find( Something.class, 1 ); + assertThat( created ).isNotNull(); + } ); } @AfterEach @@ -175,4 +324,17 @@ public String inspect(String sql) { public record InterceptorImpl(String name) implements Interceptor { } + + public static class SessionListener implements SessionEventListener { + private boolean closed; + + public boolean wasClosed() { + return closed; + } + + @Override + public void end() { + closed = true; + } + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java index 9aecfab6aa08..bf59a3d181f0 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java @@ -4,13 +4,6 @@ */ package org.hibernate.testing.orm.junit; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; - import org.hibernate.Interceptor; import org.hibernate.SessionFactoryObserver; import org.hibernate.StatelessSession; @@ -20,21 +13,27 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.StatelessSessionImplementor; import org.hibernate.internal.util.StringHelper; import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.transaction.TransactionUtil; import org.hibernate.tool.schema.Action; import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator; import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.ActionGrouping; - -import org.hibernate.testing.jdbc.SQLStatementInspector; -import org.hibernate.testing.orm.transaction.TransactionUtil; +import org.jboss.logging.Logger; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.junit.platform.commons.support.AnnotationSupport; -import org.jboss.logging.Logger; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; /** * hibernate-testing implementation of a few JUnit5 contracts to support SessionFactory-based testing, @@ -378,12 +377,12 @@ public T fromTransaction(SessionImplementor session, Function action) { + public void inStatelessSession(Consumer action) { log.trace( "#inStatelessSession(Consumer)" ); try ( final StatelessSession statelessSession = getSessionFactory().openStatelessSession() ) { log.trace( "StatelessSession opened, calling action" ); - action.accept( statelessSession ); + action.accept( (StatelessSessionImplementor) statelessSession ); } finally { log.trace( "StatelessSession close - auto-close block" ); @@ -391,12 +390,12 @@ public void inStatelessSession(Consumer action) { } @Override - public void inStatelessTransaction(Consumer action) { + public void inStatelessTransaction(Consumer action) { log.trace( "#inStatelessTransaction(Consumer)" ); try ( final StatelessSession statelessSession = getSessionFactory().openStatelessSession() ) { log.trace( "StatelessSession opened, calling action" ); - inStatelessTransaction( statelessSession, action ); + inStatelessTransaction( (StatelessSessionImplementor) statelessSession, action ); } finally { log.trace( "StatelessSession close - auto-close block" ); @@ -404,7 +403,7 @@ public void inStatelessTransaction(Consumer action) { } @Override - public void inStatelessTransaction(StatelessSession session, Consumer action) { + public void inStatelessTransaction(StatelessSessionImplementor session, Consumer action) { log.trace( "inStatelessTransaction(StatelessSession,Consumer)" ); TransactionUtil.inTransaction( session, action ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java index 45842efa1e08..96a88d0df0f8 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryScope.java @@ -7,10 +7,10 @@ import java.util.function.Consumer; import java.util.function.Function; -import org.hibernate.StatelessSession; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.StatelessSessionImplementor; import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.testing.jdbc.SQLStatementInspector; @@ -37,9 +37,9 @@ default void withSessionFactory(Consumer action) { T fromTransaction(Function action); T fromTransaction(SessionImplementor session, Function action); - void inStatelessSession(Consumer action); - void inStatelessTransaction(Consumer action); - void inStatelessTransaction(StatelessSession session, Consumer action); + void inStatelessSession(Consumer action); + void inStatelessTransaction(Consumer action); + void inStatelessTransaction(StatelessSessionImplementor session, Consumer action); void dropData(); } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java index c243b3c21656..03a46c4c3e9f 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/transaction/TransactionUtil.java @@ -14,12 +14,12 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.SharedSessionContract; -import org.hibernate.StatelessSession; import org.hibernate.Transaction; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.StatelessSessionImplementor; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.testing.orm.AsyncExecutor; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -38,7 +38,7 @@ public static void inTransaction(EntityManager entityManager, Consumer action) { + public static void inTransaction(StatelessSessionImplementor session, Consumer action) { wrapInTransaction( session, session, action ); } From bb1a8bb6bcaefe4d4f8a1bdbbba1f97d4aa8e3a2 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Tue, 30 Sep 2025 15:47:35 -0600 Subject: [PATCH 2/4] HHH-19774 - Automatic flushing for child session with shared connection/tx HHH-19808 - Automatic closing for child session with shared connection/tx --- .../engine/creation/CommonSharedBuilder.java | 1 + .../internal/ParentSessionCallbacks.java | 22 ---- .../internal/ParentSessionObserver.java | 30 +++++ .../SessionCreationOptionsAdaptor.java | 2 +- .../internal/SharedSessionBuilderImpl.java | 5 +- .../SharedSessionCreationOptions.java | 2 +- .../AbstractSharedSessionContract.java | 36 +++--- .../SessionWithSharedConnectionTest.java | 109 ++++++++---------- 8 files changed, 100 insertions(+), 107 deletions(-) delete mode 100644 hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java create mode 100644 hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonSharedBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonSharedBuilder.java index d375185e46a8..aff7b5b9cc13 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonSharedBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonSharedBuilder.java @@ -30,6 +30,7 @@ public interface CommonSharedBuilder extends CommonBuilder { /** * Signifies that the connection from the original session should be used to create the new session. + * Implies that the overall "transaction context" should be shared as well. * * @return {@code this}, for method chaining */ diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java deleted file mode 100644 index 19ead8ff2500..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionCallbacks.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.engine.creation.internal; - -/** - * Callbacks from a parent session to a child session for certain events. - * - * @author Steve Ebersole - */ -public interface ParentSessionCallbacks { - /** - * Callback when the parent is flushed. - */ - void onParentFlush(); - - /** - * Callback when the parent is closed. - */ - void onParentClose(); -} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java new file mode 100644 index 000000000000..b1545aae2f16 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.engine.creation.internal; + +import org.hibernate.engine.creation.CommonSharedBuilder; + +/** + * Allows observation of flush and closure events of a parent session from a + * child session which shares connection/transaction with the parent. + * + * @see CommonSharedBuilder#connection() + * + * @author Steve Ebersole + */ +public interface ParentSessionObserver { + /** + * Callback when the parent is flushed. Used to flush the child session. + */ + void onParentFlush(); + + /** + * Callback when the parent is closed. Used to close the child session. + * + * @apiNote Observation of closure of the parent is different from {@link org.hibernate.SessionBuilder#autoClose} + * which indicates whether the session ought to be closed in response to transaction completion. + */ + void onParentClose(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java index e420255ef70a..e66af778869c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java @@ -147,7 +147,7 @@ public TransactionCompletionCallbacksImplementor getTransactionCompletionCallbac } @Override - public void registerParentSessionCallbacks(ParentSessionCallbacks callbacks) { + public void registerParentSessionObserver(ParentSessionObserver callbacks) { originalSession.getEventListenerManager().addListener( new SessionEventListener() { @Override public void flushStart() { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java index 83c5b45896dc..67312f6bd925 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java @@ -100,6 +100,7 @@ public SessionImplementor openSession() { @Override @Deprecated(forRemoval = true) + @SuppressWarnings("removal") public SharedSessionBuilderImplementor tenantIdentifier(String tenantIdentifier) { tenantIdentifier( (Object) tenantIdentifier ); return this; @@ -261,10 +262,10 @@ public SharedSessionBuilderImplementor subselectFetchEnabled(boolean subselectFe @Override - public void registerParentSessionCallbacks(ParentSessionCallbacks callbacks) { + public void registerParentSessionObserver(ParentSessionObserver callbacks) { original.getEventListenerManager().addListener( new SessionEventListener() { @Override - public void flushStart() { + public void flushEnd(int numberOfEntities, int numberOfCollections) { callbacks.onParentFlush(); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java index c36736682835..06bcd1a18194 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java @@ -30,6 +30,6 @@ public interface SharedSessionCreationOptions extends SessionCreationOptions { /** * Registers callbacks for the child session to integrate with events of the parent session. */ - void registerParentSessionCallbacks(ParentSessionCallbacks callbacks); + void registerParentSessionObserver(ParentSessionObserver callbacks); } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index c1a78930a96d..9c2aad7269e3 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -27,7 +27,7 @@ import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.cache.spi.CacheTransactionSynchronization; import org.hibernate.dialect.Dialect; -import org.hibernate.engine.creation.internal.ParentSessionCallbacks; +import org.hibernate.engine.creation.internal.ParentSessionObserver; import org.hibernate.engine.creation.internal.SessionCreationOptions; import org.hibernate.engine.creation.internal.SessionCreationOptionsAdaptor; import org.hibernate.engine.creation.internal.SharedSessionBuilderImpl; @@ -225,7 +225,7 @@ public AbstractSharedSessionContract(SessionFactoryImpl factory, SessionCreation jdbcSessionContext = createJdbcSessionContext( statementInspector ); logInconsistentOptions( sharedOptions ); addSharedSessionTransactionObserver( transactionCoordinator ); - sharedOptions.registerParentSessionCallbacks( new ParentSessionCallbacks() { + sharedOptions.registerParentSessionObserver( new ParentSessionObserver() { @Override public void onParentFlush() { propagateFlush(); @@ -233,7 +233,10 @@ public void onParentFlush() { @Override public void onParentClose() { - propagateClose(); + // unless explicitly disabled, propagate the closure + if ( sharedOptions.shouldAutoClose() ) { + propagateClose(); + } } } ); } @@ -505,13 +508,6 @@ public boolean isClosed() { @Override public void close() { - if ( isTransactionCoordinatorShared ) { - // Perform an auto-flush - - // This deals with the natural usage pattern of a child Session - // used with a try-with-resource block - propagateFlush(); - } - if ( !closed || waitingForAutoClose ) { try { delayedAfterCompletion(); @@ -915,7 +911,7 @@ public void setNativeJdbcParametersIgnored(boolean nativeJdbcParametersIgnored) // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // dynamic HQL handling - @Override @SuppressWarnings("rawtypes") + @Override @Deprecated @SuppressWarnings({"rawtypes", "deprecation"}) public QueryImplementor createQuery(String queryString) { return createQuery( queryString, null ); } @@ -1017,10 +1013,12 @@ public QueryImplementor createQuery(TypedQueryReference typedQueryRefe checksBeforeQueryCreation(); if ( typedQueryReference instanceof SelectionSpecificationImpl specification ) { final var query = specification.buildCriteria( getCriteriaBuilder() ); + //noinspection unchecked return new SqmQueryImpl<>( (SqmStatement) query, specification.getResultType(), this ); } else if ( typedQueryReference instanceof MutationSpecificationImpl specification ) { final var query = specification.buildCriteria( getCriteriaBuilder() ); + //noinspection unchecked return new SqmQueryImpl<>( (SqmStatement) query, (Class) specification.getResultType(), this ); } else { @@ -1039,12 +1037,12 @@ else if ( typedQueryReference instanceof MutationSpecificationImpl specificat // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // dynamic native (SQL) query handling - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public NativeQueryImplementor createNativeQuery(String sqlString) { return createNativeQuery( sqlString, (Class) null ); } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public NativeQueryImplementor createNativeQuery(String sqlString, String resultSetMappingName) { checksBeforeQueryCreation(); return buildNativeQuery( sqlString, resultSetMappingName, null ); @@ -1146,7 +1144,7 @@ public QueryImplementor getNamedQuery(String queryName) { return createNamedQuery( queryName ); } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public QueryImplementor createNamedQuery(String name) { checksBeforeQueryCreation(); try { @@ -1253,12 +1251,14 @@ protected Q buildNamedQuery( // first see if it is a named HQL query final var namedSqmQueryMemento = getSqmQueryMemento( queryName ); if ( namedSqmQueryMemento != null ) { + //noinspection unchecked return sqmCreator.apply( (NamedSqmQueryMemento) namedSqmQueryMemento ); } // otherwise, see if it is a named native query final var namedNativeDescriptor = getNativeQueryMemento( queryName ); if ( namedNativeDescriptor != null ) { + //noinspection unchecked return nativeCreator.apply( (NamedNativeQueryMemento) namedNativeDescriptor ); } @@ -1322,7 +1322,7 @@ protected SqmQueryImplementor createSqmQueryImplementor(Class resultTy return query; } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public NativeQueryImplementor getNamedNativeQuery(String queryName) { final var namedNativeDescriptor = getNativeQueryMemento( queryName ); if ( namedNativeDescriptor != null ) { @@ -1333,7 +1333,7 @@ public NativeQueryImplementor getNamedNativeQuery(String queryName) { } } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public NativeQueryImplementor getNamedNativeQuery(String queryName, String resultSetMapping) { final var namedNativeDescriptor = getNativeQueryMemento( queryName ); if ( namedNativeDescriptor != null ) { @@ -1590,7 +1590,7 @@ public QueryImplementor createQuery(CriteriaQuery criteriaQuery) { } } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public QueryImplementor createQuery(@SuppressWarnings("rawtypes") CriteriaUpdate criteriaUpdate) { checkOpen(); try { @@ -1602,7 +1602,7 @@ public QueryImplementor createQuery(@SuppressWarnings("rawtypes") CriteriaUpd } } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public QueryImplementor createQuery(@SuppressWarnings("rawtypes") CriteriaDelete criteriaDelete) { checkOpen(); try { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SessionWithSharedConnectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SessionWithSharedConnectionTest.java index 97e29f5cd722..a1deb775ea16 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SessionWithSharedConnectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SessionWithSharedConnectionTest.java @@ -8,7 +8,6 @@ import org.hibernate.IrrelevantEntity; import org.hibernate.Session; import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventType; import org.hibernate.event.spi.PostInsertEvent; import org.hibernate.event.spi.PostInsertEventListener; @@ -29,6 +28,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -37,8 +37,9 @@ /** * @author Steve Ebersole */ +@SuppressWarnings("JUnitMalformedDeclaration") @DomainModel(annotatedClasses = IrrelevantEntity.class) -@SessionFactory +@SessionFactory(useCollectingStatementInspector = true) public class SessionWithSharedConnectionTest { @Test @JiraKey( value = "HHH-7090" ) @@ -53,7 +54,6 @@ public void testSharedTransactionContextSessionClosing(SessionFactoryScope scope CriteriaQuery criteria = criteriaBuilder.createQuery( IrrelevantEntity.class ); criteria.from( IrrelevantEntity.class ); session.createQuery( criteria ).list(); -// secondSession.createCriteria( IrrelevantEntity.class ).list(); //the list should have registered and then released a JDBC resource assertFalse( ((SessionImplementor) secondSession) @@ -82,28 +82,28 @@ public void testSharedTransactionContextSessionClosing(SessionFactoryScope scope @Test @JiraKey( value = "HHH-7090" ) public void testSharedTransactionContextAutoClosing(SessionFactoryScope scope) { - Session session = scope.getSessionFactory().openSession(); + var session = scope.getSessionFactory().openSession(); session.getTransaction().begin(); // COMMIT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Session secondSession = session.sessionWithOptions() + var secondSession = (SessionImplementor) session.sessionWithOptions() .connection() .autoClose( true ) .openSession(); // directly assert state of the second session - assertTrue( ((SessionImpl) secondSession).isAutoCloseSessionEnabled() ); + assertTrue( secondSession.isAutoCloseSessionEnabled() ); assertTrue( ((SessionImpl) secondSession).shouldAutoClose() ); // now commit the transaction and make sure that does not close the sessions session.getTransaction().commit(); - assertFalse( ((SessionImplementor) session).isClosed() ); - assertTrue( ((SessionImplementor) secondSession).isClosed() ); + assertFalse( session.isClosed() ); + assertTrue( secondSession.isClosed() ); session.close(); - assertTrue( ((SessionImplementor) session).isClosed() ); - assertTrue( ((SessionImplementor) secondSession).isClosed() ); + assertTrue( session.isClosed() ); + assertTrue( secondSession.isClosed() ); // ROLLBACK ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -111,74 +111,52 @@ public void testSharedTransactionContextAutoClosing(SessionFactoryScope scope) { session = scope.getSessionFactory().openSession(); session.getTransaction().begin(); - secondSession = session.sessionWithOptions() + secondSession = (SessionImplementor) session.sessionWithOptions() .connection() .autoClose( true ) .openSession(); // directly assert state of the second session - assertTrue( ((SessionImpl) secondSession).isAutoCloseSessionEnabled() ); + assertTrue( secondSession.isAutoCloseSessionEnabled() ); assertTrue( ((SessionImpl) secondSession).shouldAutoClose() ); // now rollback the transaction and make sure that does not close the sessions session.getTransaction().rollback(); - assertFalse( ((SessionImplementor) session).isClosed() ); - assertTrue( ((SessionImplementor) secondSession).isClosed() ); + assertFalse( session.isClosed() ); + assertTrue( secondSession.isClosed() ); session.close(); - assertTrue( ((SessionImplementor) session).isClosed() ); - assertTrue( ((SessionImplementor) secondSession).isClosed() ); + assertTrue( session.isClosed() ); + assertTrue( secondSession.isClosed() ); } -// @Test -// @JiraKey( value = "HHH-7090" ) -// public void testSharedTransactionContextAutoJoining() { -// Session session = scope.getSessionFactory().openSession(); -// session.getTransaction().begin(); -// -// Session secondSession = session.sessionWithOptions() -// .transactionContext() -// .autoJoinTransactions( true ) -// .openSession(); -// -// // directly assert state of the second session -// assertFalse( ((SessionImplementor) secondSession).shouldAutoJoinTransaction() ); -// -// secondSession.close(); -// session.close(); -// } - @Test @JiraKey( value = "HHH-7090" ) public void testSharedTransactionContextFlushBeforeCompletion(SessionFactoryScope scope) { - Session session = scope.getSessionFactory().openSession(); + var session = scope.getSessionFactory().openSession(); session.getTransaction().begin(); - Session secondSession = session.sessionWithOptions() + var secondSession = (SessionImplementor) session.sessionWithOptions() .connection() -// .flushBeforeCompletion( true ) .autoClose( true ) .openSession(); - // directly assert state of the second session -// assertTrue( ((SessionImplementor) secondSession).isFlushBeforeCompletionEnabled() ); - // now try it out IrrelevantEntity irrelevantEntity = new IrrelevantEntity(); secondSession.persist( irrelevantEntity ); Integer id = irrelevantEntity.getId(); session.getTransaction().commit(); - assertFalse( ((SessionImplementor) session).isClosed() ); - assertTrue( ((SessionImplementor) secondSession).isClosed() ); + assertFalse( session.isClosed() ); + assertTrue( secondSession.isClosed() ); session.close(); - assertTrue( ((SessionImplementor) session).isClosed() ); - assertTrue( ((SessionImplementor) secondSession).isClosed() ); + assertTrue( session.isClosed() ); + assertTrue( secondSession.isClosed() ); session = scope.getSessionFactory().openSession(); session.getTransaction().begin(); - IrrelevantEntity it = session.byId( IrrelevantEntity.class ).load( id ); + IrrelevantEntity it = session.find( IrrelevantEntity.class, id ); assertNotNull( it ); session.remove( it ); session.getTransaction().commit(); @@ -188,11 +166,12 @@ public void testSharedTransactionContextFlushBeforeCompletion(SessionFactoryScop @Test @JiraKey( value = "HHH-7239" ) public void testChildSessionCallsAfterTransactionAction(SessionFactoryScope scope) throws Exception { - Session session = scope.getSessionFactory().openSession(); + final var sqlCollector = scope.getCollectingStatementInspector(); + sqlCollector.clear(); - final String postCommitMessage = "post commit was called"; + final var postCommitMessage = "post commit was called"; - EventListenerRegistry eventListenerRegistry = scope.getSessionFactory().getEventListenerRegistry(); + var eventListenerRegistry = scope.getSessionFactory().getEventListenerRegistry(); //register a post commit listener eventListenerRegistry.appendListeners( EventType.POST_COMMIT_INSERT, @@ -209,28 +188,33 @@ public boolean requiresPostCommitHandling(EntityPersister persister) { } ); - session.getTransaction().begin(); + final var parentSession = scope.getSessionFactory().openSession(); + parentSession.beginTransaction(); - IrrelevantEntity irrelevantEntityMainSession = new IrrelevantEntity(); - irrelevantEntityMainSession.setName( "main session" ); - session.persist( irrelevantEntityMainSession ); + var mainEntity = new IrrelevantEntity(); + mainEntity.setName( "main session" ); + parentSession.persist( mainEntity ); - //open secondary session to also insert an entity - Session secondSession = session.sessionWithOptions() + // open child session to also insert an entity + var childSession = parentSession.sessionWithOptions() .connection() -// .flushBeforeCompletion( true ) .autoClose( true ) .openSession(); - IrrelevantEntity irrelevantEntitySecondarySession = new IrrelevantEntity(); - irrelevantEntitySecondarySession.setName( "secondary session" ); - secondSession.persist( irrelevantEntitySecondarySession ); + var childEntity = new IrrelevantEntity(); + childEntity.setName( "secondary session" ); + childSession.persist( childEntity ); - session.getTransaction().commit(); + parentSession.getTransaction().commit(); + + assertThat( sqlCollector.getSqlQueries() ).hasSize( 3 ); + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).startsWith( "select max(id) " ); + assertThat( sqlCollector.getSqlQueries().get( 1 ) ).startsWith( "insert" ); + assertThat( sqlCollector.getSqlQueries().get( 2 ) ).startsWith( "insert" ); - //both entities should have their names updated to the postCommitMessage value - assertEquals( postCommitMessage, irrelevantEntityMainSession.getName() ); - assertEquals( postCommitMessage, irrelevantEntitySecondarySession.getName() ); + // both entities should have their names updated to the postCommitMessage value + assertThat( mainEntity.getName() ).isEqualTo( postCommitMessage ); + assertThat( childEntity.getName() ).isEqualTo( postCommitMessage ); } @Test @@ -243,7 +227,6 @@ public void testChildSessionTwoTransactions(SessionFactoryScope scope) { //open secondary session with managed options Session secondarySession = session.sessionWithOptions() .connection() -// .flushBeforeCompletion( true ) .autoClose( true ) .openSession(); From abc7348e451b4f1fa8c7a7c686f160c7ad1b906a Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Tue, 30 Sep 2025 20:20:24 -0600 Subject: [PATCH 3/4] HHH-19774 - Automatic flushing for child session with shared connection/tx HHH-19808 - Automatic closing for child session with shared connection/tx --- .../internal/ParentSessionObserver.java | 3 - .../internal/DefaultFlushEventListener.java | 52 +++++++------- .../AbstractSharedSessionContract.java | 5 +- .../SimpleSharedSessionBuildingTests.java | 68 ++++++++----------- 4 files changed, 57 insertions(+), 71 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java index b1545aae2f16..f0f2bfadc76c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java @@ -22,9 +22,6 @@ public interface ParentSessionObserver { /** * Callback when the parent is closed. Used to close the child session. - * - * @apiNote Observation of closure of the parent is different from {@link org.hibernate.SessionBuilder#autoClose} - * which indicates whether the session ought to be closed in response to transaction completion. */ void onParentClose(); } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEventListener.java index 568566433e33..3594e8363fc0 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEventListener.java @@ -24,38 +24,40 @@ public class DefaultFlushEventListener extends AbstractFlushingEventListener imp */ public void onFlush(FlushEvent event) throws HibernateException { final var source = event.getSession(); - final var persistenceContext = source.getPersistenceContextInternal(); + final var eventMonitor = source.getEventMonitor(); - if ( persistenceContext.getNumberOfManagedEntities() > 0 - || persistenceContext.getCollectionEntriesSize() > 0 ) { - EVENT_LISTENER_LOGGER.executingFlush(); - final var flushEvent = eventMonitor.beginFlushEvent(); - final var eventListenerManager = source.getEventListenerManager(); - try { - eventListenerManager.flushStart(); + final var flushEvent = eventMonitor.beginFlushEvent(); + + final var eventListenerManager = source.getEventListenerManager(); + eventListenerManager.flushStart(); + + try { + final var persistenceContext = source.getPersistenceContextInternal(); + if ( persistenceContext.getNumberOfManagedEntities() > 0 + || persistenceContext.getCollectionEntriesSize() > 0 ) { + EVENT_LISTENER_LOGGER.executingFlush(); flushEverythingToExecutions( event ); performExecutions( source ); postFlush( source ); - } - finally { - eventMonitor.completeFlushEvent( flushEvent, event ); - eventListenerManager.flushEnd( - event.getNumberOfEntitiesProcessed(), - event.getNumberOfCollectionsProcessed() - ); - } - - postPostFlush( source ); + postPostFlush( source ); - final var statistics = source.getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.flush(); + final var statistics = source.getFactory().getStatistics(); + if ( statistics.isStatisticsEnabled() ) { + statistics.flush(); + } + } + else if ( source.getActionQueue().hasAnyQueuedActions() ) { + EVENT_LISTENER_LOGGER.executingFlush(); + // execute any queued unloaded-entity deletions + performExecutions( source ); } } - else if ( source.getActionQueue().hasAnyQueuedActions() ) { - EVENT_LISTENER_LOGGER.executingFlush(); - // execute any queued unloaded-entity deletions - performExecutions( source ); + finally { + eventMonitor.completeFlushEvent( flushEvent, event ); + eventListenerManager.flushEnd( + event.getNumberOfEntitiesProcessed(), + event.getNumberOfCollectionsProcessed() + ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 9c2aad7269e3..fa06b7453326 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -233,10 +233,7 @@ public void onParentFlush() { @Override public void onParentClose() { - // unless explicitly disabled, propagate the closure - if ( sharedOptions.shouldAutoClose() ) { - propagateClose(); - } + propagateClose(); } } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java index 84f74e9083aa..23d8c43bdad0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sharedSession/SimpleSharedSessionBuildingTests.java @@ -184,58 +184,38 @@ void testClosePropagation(SessionFactoryScope factoryScope) { */ @Test void testAutoFlushStatefulChild(SessionFactoryScope factoryScope) { - final var sqlCollector = factoryScope.getCollectingStatementInspector(); - sqlCollector.clear(); - - factoryScope.inTransaction( (parentSession) -> { - try (SessionImplementor childSession = (SessionImplementor) parentSession - .sessionWithOptions() - .connection() - .openSession()) { - // persist an entity through the child session - - // should be auto flushed (technically as part of the try-with-resources close of the child session) - childSession.persist( new Something( 1, "first" ) ); - } - } ); - - // make sure the flush and insert happened - assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); - factoryScope.inTransaction( (session) -> { - final Something created = session.find( Something.class, 1 ); - assertThat( created ).isNotNull(); - } ); - - } + final MutableObject parentSessionRef = new MutableObject<>(); + final MutableObject childSessionRef = new MutableObject<>(); - /** - * NOTE: builds on assertions from {@link #testConnectionAndTransactionSharing} - * and {@linkplain #testClosePropagation} - */ - @Test - void testAutoFlushStatefulChildNoClose(SessionFactoryScope factoryScope) { final var sqlCollector = factoryScope.getCollectingStatementInspector(); sqlCollector.clear(); factoryScope.inTransaction( (parentSession) -> { - SessionImplementor childSession = (SessionImplementor) parentSession + parentSessionRef.set( parentSession ); + + // IMPORTANT: it is important that the child session not be closed here (e.g. try-with-resources). + final SessionImplementor childSession = (SessionImplementor) parentSession .sessionWithOptions() .connection() .openSession(); + childSessionRef.set( childSession ); - // persist an entity through the shared/child session. - // then make sure the auto-flush of the parent session - // propagates to the shared/child + // persist an entity through the child session - should be auto flushed as part of the + // parent session's flush cycle childSession.persist( new Something( 1, "first" ) ); } ); + assertThat( parentSessionRef.get().isClosed() ).isTrue(); + assertThat( childSessionRef.get().isClosed() ).isTrue(); + // make sure the flush and insert happened assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); factoryScope.inTransaction( (session) -> { final Something created = session.find( Something.class, 1 ); assertThat( created ).isNotNull(); } ); - } + } /** @@ -243,20 +223,30 @@ void testAutoFlushStatefulChildNoClose(SessionFactoryScope factoryScope) { */ @Test void testAutoFlushStatelessChild(SessionFactoryScope factoryScope) { + final MutableObject parentSessionRef = new MutableObject<>(); + final MutableObject childSessionRef = new MutableObject<>(); + final var sqlCollector = factoryScope.getCollectingStatementInspector(); sqlCollector.clear(); factoryScope.inStatelessTransaction( (parentSession) -> { - try (SessionImplementor childSession = (SessionImplementor) parentSession + parentSessionRef.set( parentSession ); + + // IMPORTANT: it is important that the child session not be closed here (e.g. try-with-resources). + final var childSession = (SessionImplementor) parentSession .sessionWithOptions() .connection() - .openSession()) { - // persist an entity through the child session - - // should be auto flushed (technically as part of the try-with-resources close of the child session) - childSession.persist( new Something( 1, "first" ) ); - } + .openSession(); + childSessionRef.set( childSession ); + + // persist an entity through the child session - should be auto flushed as part of the + // parent session's flush cycle + childSession.persist( new Something( 1, "first" ) ); } ); + assertThat( parentSessionRef.get().isClosed() ).isTrue(); + assertThat( childSessionRef.get().isClosed() ).isTrue(); + // make sure the flush and insert happened assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); factoryScope.inTransaction( (session) -> { From eddc9b918e42b96e95d806b98482d514b4429af6 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 1 Oct 2025 10:15:56 -0600 Subject: [PATCH 4/4] HHH-19774 - Automatic flushing for child session with shared connection/tx HHH-19808 - Automatic closing for child session with shared connection/tx --- .../SessionCreationOptionsAdaptor.java | 14 +---- .../internal/SharedSessionBuilderImpl.java | 15 +---- .../SharedSessionCreationOptions.java | 26 +++++++- .../hibernate/event/jfr/flush/FlushTests.java | 30 +++++----- migration-guide.adoc | 60 ++++++++++++++++--- whats-new.adoc | 19 ++++++ 6 files changed, 115 insertions(+), 49 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java index e66af778869c..1b6995be6b63 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java @@ -147,17 +147,7 @@ public TransactionCompletionCallbacksImplementor getTransactionCompletionCallbac } @Override - public void registerParentSessionObserver(ParentSessionObserver callbacks) { - originalSession.getEventListenerManager().addListener( new SessionEventListener() { - @Override - public void flushStart() { - callbacks.onParentFlush(); - } - - @Override - public void end() { - callbacks.onParentClose(); - } - } ); + public void registerParentSessionObserver(ParentSessionObserver observer) { + registerParentSessionObserver( observer, originalSession ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java index 67312f6bd925..8f529e21436e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java @@ -260,20 +260,9 @@ public SharedSessionBuilderImplementor subselectFetchEnabled(boolean subselectFe // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SharedSessionCreationOptions - @Override - public void registerParentSessionObserver(ParentSessionObserver callbacks) { - original.getEventListenerManager().addListener( new SessionEventListener() { - @Override - public void flushEnd(int numberOfEntities, int numberOfCollections) { - callbacks.onParentFlush(); - } - - @Override - public void end() { - callbacks.onParentClose(); - } - } ); + public void registerParentSessionObserver(ParentSessionObserver observer) { + registerParentSessionObserver( observer, original ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java index 06bcd1a18194..ae7c53ae7f19 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionCreationOptions.java @@ -4,8 +4,10 @@ */ package org.hibernate.engine.creation.internal; +import org.hibernate.SessionEventListener; import org.hibernate.Transaction; import org.hibernate.engine.jdbc.spi.JdbcCoordinator; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.TransactionCompletionCallbacksImplementor; import org.hibernate.resource.transaction.spi.TransactionCoordinator; @@ -30,6 +32,28 @@ public interface SharedSessionCreationOptions extends SessionCreationOptions { /** * Registers callbacks for the child session to integrate with events of the parent session. */ - void registerParentSessionObserver(ParentSessionObserver callbacks); + void registerParentSessionObserver(ParentSessionObserver observer); + + /** + * Consolidated implementation of adding the parent session observer. + */ + default void registerParentSessionObserver(ParentSessionObserver observer, SharedSessionContractImplementor original) { + original.getEventListenerManager().addListener( new SessionEventListener() { + @Override + public void flushEnd(int numberOfEntities, int numberOfCollections) { + observer.onParentFlush(); + } + + @Override + public void partialFlushEnd(int numberOfEntities, int numberOfCollections) { + observer.onParentFlush(); + } + + @Override + public void end() { + observer.onParentClose(); + } + } ); + } } diff --git a/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/FlushTests.java b/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/FlushTests.java index 3f69e8b775d3..f79b1c1bb82a 100644 --- a/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/FlushTests.java +++ b/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/FlushTests.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@SuppressWarnings("JUnitMalformedDeclaration") @JfrEventTest @DomainModel(annotatedClasses = { FlushTests.TestEntity.class, @@ -67,22 +68,21 @@ public void testFlushEvent(SessionFactoryScope scope) { @Test @EnableEvent(JdbcBatchExecutionEvent.NAME) - public void testFlushNoFired(SessionFactoryScope scope) { + public void testFlushNotFired(SessionFactoryScope scope) { jfrEvents.reset(); - scope.inTransaction( - session -> { - - } - ); - List events = jfrEvents.events() - .filter( - recordedEvent -> - { - String eventName = recordedEvent.getEventType().getName(); - return eventName.equals( FlushEvent.NAME ); - } - ).toList(); - assertThat( events ).hasSize( 0 ); + scope.inTransaction( (session) -> { + // do nothing + } ); + List events = jfrEvents.events().filter( (recordedEvent) -> { + String eventName = recordedEvent.getEventType().getName(); + return eventName.equals( FlushEvent.NAME ); + } ).toList(); + + // the flush event should be triggered by the commit + assertThat( events ).hasSize( 1 ); + // however, it should not affect anything + assertThat( events.get( 0 ).getInt( "numberOfEntitiesProcessed" ) ).isEqualTo( 0 ); + assertThat( events.get( 0 ).getInt( "numberOfCollectionsProcessed" ) ).isEqualTo( 0 ); } @Entity(name = "TestEntity") diff --git a/migration-guide.adoc b/migration-guide.adoc index e33a026fc6fc..e0f69197d283 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -40,13 +40,6 @@ See the link:{releaseSeriesBase}#whats-new[website] for the list of new features This section describes changes to contracts (classes, interfaces, methods, etc.) which are considered https://hibernate.org/community/compatibility-policy/#api[API]. -* Code underlying the session builder APIs was reengineered, and the behavior of `noInterceptor()` for `SharedSessionBuilder` and `SharedStatelessSessionBuilder` was aligned with the preexisting semantics of this method on `SessionBuilder` and `StatelessSessionBuilder`. - The previous behavior may be recovered by calling `noSessionInterceptorCreation()`. - -* `org.hibernate.dialect.AzureSQLServerDialect` was deprecated. Use `org.hibernate.dialect.SQLServerDialect` instead. - If you set `hibernate.boot.allow_jdbc_metadata_access=false` for offline startup, - remember to also set the targeted database version through `jakarta.persistence.database-product-version`; - this would be 16.0 for SQL Server 2022 or 17.0 for SQL Server 2025. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SPI changes @@ -57,7 +50,58 @@ This section describes changes to contracts (classes, interfaces, methods, etc.) This section describes changes to contracts (classes, interfaces, methods, etc.) which are considered https://hibernate.org/community/compatibility-policy/#spi[SPI]. -* Some operations of `TypeConfiguration`, `JavaTypeRegistry`, and `BasicTypeRegistry` used unbound type parameters in the return type. The generic signatures of these methods have been changed for improved type safety. +[[registry-generic-signatures]] +=== Registry Generic Signatures + +Some operations of `TypeConfiguration`, `JavaTypeRegistry`, and `BasicTypeRegistry` had previously used unbound type parameters in the return type. The generic signatures of these methods have been changed for improved type safety. + + +[[AzureSQLServerDialect]] +=== AzureSQLServerDialect Deprecation + +`org.hibernate.dialect.AzureSQLServerDialect` was deprecated; use `org.hibernate.dialect.SQLServerDialect` instead. + +[IMPORTANT] +==== +If you set `hibernate.boot.allow_jdbc_metadata_access=false` for offline startup, +remember to also set the targeted database version through `jakarta.persistence.database-product-version` - this would be 16.0 for SQL Server 2022 or 17.0 for SQL Server 2025. +==== + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Changes in Behavior +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +[[behavior-changes]] +== Changes in Behavior + +[[child-session-flush-close]] +=== Child Session Flush/Close Behavior + +`Session` and `StatelessSession` which share transactional context with a parent now have slightly different semantics in regard to flushing and closing - + +* when the parent is flushed, the child is flushed +* when the parent is closed, the child is closed + +[NOTE] +==== +This led to a change in triggering of flush events for both - + +* `SessionEventListener` registrations +* JFR events + +In both cases, the events are now triggered regardless of whether any entities or collections were actually flushed. +Each already carried the number of entities and the number of collections which were actually flushed. +Previously, when no entities and no collections were flushed to the database no event was generated; the event is now generated and both values will be zero. + +Interestingly, this now also aligns with handling for auto-flush events which already always triggered these events. +==== + + +[[child-session-no-interceptor]] +=== Child Session No-Interceptor Behavior + +The behavior of `noInterceptor()` for `SharedSessionBuilder` and (the new) `SharedStatelessSessionBuilder` was aligned with the preexisting semantics of this method on `SessionBuilder` and `StatelessSessionBuilder`. +The previous behavior may be recovered by calling `noSessionInterceptorCreation()`. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/whats-new.adoc b/whats-new.adoc index 311f5aaa7689..e6bb7886734c 100644 --- a/whats-new.adoc +++ b/whats-new.adoc @@ -11,6 +11,25 @@ Describes the new features and capabilities added to Hibernate ORM in {version}. IMPORTANT: If migrating from earlier versions, be sure to also check out the link:{migrationGuide}[Migration Guide] for discussion of impactful changes. +[[child-stateless-sessions]] +== Child StatelessSession + +Creation of child `StatelessSession` is now supported, just as with child `Session`. +This is a `StatelessSession` which shares "transactional context" with a parent `Session` or `StatelessSession`. +Use `Session#statelessWithOptions` or `StatelessSession#statelessWithOptions` instead of `#sessionWithOptions`. + +==== +[source,java] +---- +Session parent = ...; +StatelessSession child = parent + .statelessWithOptions() + .connection() + ... + .open(); +---- +==== + [[vector-module-enhancements]] == Hibernate-Vector module enhancements