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/ParentSessionObserver.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java new file mode 100644 index 000000000000..f0f2bfadc76c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/ParentSessionObserver.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + 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..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 @@ -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,9 @@ public Transaction getTransaction() { public TransactionCompletionCallbacksImplementor getTransactionCompletionCallbacks() { return options.getTransactionCompletionCallbacksImplementor(); } + + @Override + 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 7b0fee5308e2..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 @@ -100,6 +100,7 @@ public SessionImplementor openSession() { @Override @Deprecated(forRemoval = true) + @SuppressWarnings("removal") public SharedSessionBuilderImplementor tenantIdentifier(String tenantIdentifier) { tenantIdentifier( (Object) tenantIdentifier ); return this; @@ -259,6 +260,11 @@ public SharedSessionBuilderImplementor subselectFetchEnabled(boolean subselectFe // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SharedSessionCreationOptions + @Override + public void registerParentSessionObserver(ParentSessionObserver observer) { + registerParentSessionObserver( observer, original ); + } + @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..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; @@ -26,4 +28,32 @@ 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 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-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 822b287dc180..fa06b7453326 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.ParentSessionObserver; 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.registerParentSessionObserver( new ParentSessionObserver() { + @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 ) ); } }; } @@ -538,6 +549,10 @@ protected void setClosed() { cleanupOnClose(); } + protected abstract void propagateFlush(); + + protected abstract void propagateClose(); + protected void checkBeforeClosingJdbcCoordinator() { } @@ -893,7 +908,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 ); } @@ -995,10 +1010,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 { @@ -1017,12 +1034,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 ); @@ -1124,7 +1141,7 @@ public QueryImplementor getNamedQuery(String queryName) { return createNamedQuery( queryName ); } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public QueryImplementor createNamedQuery(String name) { checksBeforeQueryCreation(); try { @@ -1231,12 +1248,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 ); } @@ -1300,7 +1319,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 ) { @@ -1311,7 +1330,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 ) { @@ -1568,7 +1587,7 @@ public QueryImplementor createQuery(CriteriaQuery criteriaQuery) { } } - @Override + @Override @Deprecated @SuppressWarnings("deprecation") public QueryImplementor createQuery(@SuppressWarnings("rawtypes") CriteriaUpdate criteriaUpdate) { checkOpen(); try { @@ -1580,7 +1599,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/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/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(); 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..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 @@ -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,180 @@ 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 ); - // try from Session - sqlCollector.clear(); - factoryScope.inTransaction( (statefulSession) -> { - try (var session = statefulSession + // child stateful session + 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! + 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(); + + 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 MutableObject parentSessionRef = new MutableObject<>(); + final MutableObject childSessionRef = new MutableObject<>(); + + final var sqlCollector = factoryScope.getCollectingStatementInspector(); + sqlCollector.clear(); + + factoryScope.inTransaction( (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 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(); + } ); - // try from StatelessSession + } + + + /** + * NOTE: builds on assertions from {@link #testConnectionAndTransactionSharing} + */ + @Test + void testAutoFlushStatelessChild(SessionFactoryScope factoryScope) { + final MutableObject parentSessionRef = new MutableObject<>(); + final MutableObject childSessionRef = new MutableObject<>(); + + final var sqlCollector = factoryScope.getCollectingStatementInspector(); sqlCollector.clear(); - factoryScope.inStatelessTransaction( (statelessSession) -> { - try (var statefulSession = statelessSession + + factoryScope.inStatelessTransaction( (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()) { - 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! - } + .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) -> { + 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 +314,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-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/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 ); } 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