From f27b93edbd28c468fd98720072ec7e7f707938a7 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 10:15:53 +0100 Subject: [PATCH 1/7] minor improvement to jdoc --- .../event/internal/AbstractFlushingEventListener.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java index dba92f4c45db..570ca0210e2a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java @@ -113,9 +113,9 @@ protected void logFlushResults(FlushEvent event) { } /** - * Process cascade save/update at the start of a flush to discover - * any newly referenced entity that must be passed to saveOrUpdate(), - * and also apply orphan delete + * Process {@link CascadingActions#PERSIST_ON_FLUSH} at the start of a + * flush to discover any newly referenced entity that must be passed to + * {@code persist()}, and also apply orphan delete. */ private void prepareEntityFlushes(EventSource session, PersistenceContext persistenceContext) throws HibernateException { From 9deeffc663b9a32a855ed980a54d759bec2280cc Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 10:16:10 +0100 Subject: [PATCH 2/7] very minor code changes in ActionQueue --- .../java/org/hibernate/engine/spi/ActionQueue.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java index 792497dd1805..ed1689a7d5ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java @@ -588,25 +588,20 @@ public boolean areTablesToBeUpdated(Set tables) { return true; } } - if ( unresolvedInsertions == null ) { - return false; - } - return areTablesToBeUpdated( unresolvedInsertions, tables ); + return unresolvedInsertions != null + && areTablesToBeUpdated( unresolvedInsertions, tables ); } private static boolean areTablesToBeUpdated(@Nullable ExecutableList queue, Set tableSpaces) { - if ( queue == null || queue.isEmpty() ) { - return false; - } - else { + if ( queue != null && !queue.isEmpty() ) { for ( var actionSpace : queue.getQuerySpaces() ) { if ( tableSpaces.contains( actionSpace ) ) { ACTION_LOGGER.changesMustBeFlushedToSpace( actionSpace ); return true; } } - return false; } + return false; } private static boolean areTablesToBeUpdated(UnresolvedEntityInsertActions actions, Set tableSpaces) { From aad3c3394e97f25bf0243d4e80d01fb19bf3af8b Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 10:16:41 +0100 Subject: [PATCH 3/7] optimize auto-flush when there are no query spaces suggested by user Thien --- .../DefaultAutoFlushEventListener.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java index c658bc905aa3..9ebce007dcde 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java @@ -34,8 +34,7 @@ public void onAutoFlush(AutoFlushEvent event) throws HibernateException { final var partialFlushEvent = eventMonitor.beginPartialFlushEvent(); try { eventListenerManager.partialFlushStart(); - - if ( flushMightBeNeeded( source ) ) { + if ( flushMightBeNeeded( event, source ) ) { // Need to get the number of collection removals before flushing to executions // (because flushing to executions can add collection removal actions to the action queue). final var actionQueue = source.getActionQueue(); @@ -89,7 +88,7 @@ public void onAutoPreFlush(EventSource source) throws HibernateException { final var eventMonitor = source.getEventMonitor(); final var diagnosticEvent = eventMonitor.beginPrePartialFlush(); try { - if ( flushMightBeNeeded( source ) ) { + if ( flushMightBeNeeded( null, source ) ) { preFlush( source, source.getPersistenceContextInternal() ); } } @@ -99,15 +98,27 @@ public void onAutoPreFlush(EventSource source) throws HibernateException { } } - private boolean flushIsReallyNeeded(AutoFlushEvent event, final EventSource source) { + private boolean flushIsReallyNeeded(AutoFlushEvent event, EventSource source) { return source.getHibernateFlushMode() == FlushMode.ALWAYS || source.getActionQueue().areTablesToBeUpdated( event.getQuerySpaces() ); } - private boolean flushMightBeNeeded(final EventSource source) { + private boolean flushMightBeNeeded(AutoFlushEvent event, EventSource source) { + return flushMightBeNeededForMode( event, source ) + && nonEmpty( source ); + } + + private static boolean flushMightBeNeededForMode(AutoFlushEvent event, EventSource source) { + return switch ( source.getHibernateFlushMode() ) { + case ALWAYS -> true; + case AUTO -> event == null || !event.getQuerySpaces().isEmpty(); + case MANUAL, COMMIT -> false; + }; + } + + private static boolean nonEmpty(EventSource source) { final var persistenceContext = source.getPersistenceContextInternal(); - return !source.getHibernateFlushMode().lessThan( FlushMode.AUTO ) - && ( persistenceContext.getNumberOfManagedEntities() > 0 - || persistenceContext.getCollectionEntriesSize() > 0 ); + return persistenceContext.getNumberOfManagedEntities() > 0 + || persistenceContext.getCollectionEntriesSize() > 0; } } From 76ebaeeb471ccd5a3fcfc1ccade0dce6fc0bce9c Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 13:10:31 +0100 Subject: [PATCH 4/7] skip the pre-flush when unnecessary --- .../engine/spi/SessionDelegatorBaseImpl.java | 4 +- .../spi/SharedSessionContractImplementor.java | 2 +- .../spi/SharedSessionDelegatorBaseImpl.java | 4 +- .../DefaultAutoFlushEventListener.java | 26 ++----- .../DefaultPreFlushEventListener.java | 31 ++++++++ .../internal/EventListenerRegistryImpl.java | 5 ++ .../service/spi/EventListenerGroups.java | 3 + .../event/spi/AutoFlushEventListener.java | 3 - .../org/hibernate/event/spi/EventType.java | 1 + .../event/spi/PreFlushEventListener.java | 15 ++++ .../org/hibernate/internal/SessionImpl.java | 9 ++- .../internal/StatelessSessionImpl.java | 2 +- .../internal/ProcedureParamBindings.java | 5 ++ .../internal/QueryParameterBindingsImpl.java | 31 ++++++++ .../query/spi/QueryParameterBindings.java | 7 ++ .../internal/ConcreteSqmSelectQueryPlan.java | 77 +++++++++++-------- 16 files changed, 159 insertions(+), 66 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java create mode 100644 hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index abcfa37af833..d0bce95054ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -418,8 +418,8 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush() { - delegate.autoPreFlush(); + public void autoPreFlush(Set querySpaces) { + delegate.autoPreFlush( querySpaces ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java index 958e71319ab4..3ae535a0f43f 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java @@ -535,7 +535,7 @@ default boolean autoFlushIfRequired(Set querySpaces) { */ boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush); - void autoPreFlush(); + void autoPreFlush(Set querySpaces); /** * Check if there is a Hibernate or JTA transaction in progress and, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 4f7f1951c05f..db7eedf9d878 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -569,8 +569,8 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush() { - delegate.autoPreFlush(); + public void autoPreFlush(Set querySpaces) { + delegate.autoPreFlush( querySpaces ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java index 9ebce007dcde..30ea842b27a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java @@ -81,29 +81,12 @@ public void onAutoFlush(AutoFlushEvent event) throws HibernateException { } } - @Override - public void onAutoPreFlush(EventSource source) throws HibernateException { - final var eventListenerManager = source.getEventListenerManager(); - eventListenerManager.prePartialFlushStart(); - final var eventMonitor = source.getEventMonitor(); - final var diagnosticEvent = eventMonitor.beginPrePartialFlush(); - try { - if ( flushMightBeNeeded( null, source ) ) { - preFlush( source, source.getPersistenceContextInternal() ); - } - } - finally { - eventMonitor.completePrePartialFlush( diagnosticEvent, source ); - eventListenerManager.prePartialFlushEnd(); - } - } - - private boolean flushIsReallyNeeded(AutoFlushEvent event, EventSource source) { + static boolean flushIsReallyNeeded(AutoFlushEvent event, EventSource source) { return source.getHibernateFlushMode() == FlushMode.ALWAYS || source.getActionQueue().areTablesToBeUpdated( event.getQuerySpaces() ); } - private boolean flushMightBeNeeded(AutoFlushEvent event, EventSource source) { + static boolean flushMightBeNeeded(AutoFlushEvent event, EventSource source) { return flushMightBeNeededForMode( event, source ) && nonEmpty( source ); } @@ -111,7 +94,10 @@ private boolean flushMightBeNeeded(AutoFlushEvent event, EventSource source) { private static boolean flushMightBeNeededForMode(AutoFlushEvent event, EventSource source) { return switch ( source.getHibernateFlushMode() ) { case ALWAYS -> true; - case AUTO -> event == null || !event.getQuerySpaces().isEmpty(); + case AUTO -> { + final var querySpaces = event.getQuerySpaces(); + yield querySpaces == null || !querySpaces.isEmpty(); + } case MANUAL, COMMIT -> false; }; } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java new file mode 100644 index 000000000000..9f42d745fc31 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.event.internal; + +import org.hibernate.HibernateException; +import org.hibernate.event.spi.AutoFlushEvent; +import org.hibernate.event.spi.PreFlushEventListener; + +import static org.hibernate.event.internal.DefaultAutoFlushEventListener.flushMightBeNeeded; + +public class DefaultPreFlushEventListener extends AbstractFlushingEventListener implements PreFlushEventListener { + @Override + public void onAutoPreFlush(AutoFlushEvent event) throws HibernateException { + final var source = event.getEventSource(); + final var eventListenerManager = source.getEventListenerManager(); + eventListenerManager.prePartialFlushStart(); + final var eventMonitor = source.getEventMonitor(); + final var diagnosticEvent = eventMonitor.beginPrePartialFlush(); + try { + if ( flushMightBeNeeded( event, source ) ) { + preFlush( source, source.getPersistenceContextInternal() ); + } + } + finally { + eventMonitor.completePrePartialFlush( diagnosticEvent, source ); + eventListenerManager.prePartialFlushEnd(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java b/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java index f1c290ec07dc..2bda60e2a49b 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java @@ -25,6 +25,7 @@ import org.hibernate.event.internal.DefaultPersistEventListener; import org.hibernate.event.internal.DefaultPersistOnFlushEventListener; import org.hibernate.event.internal.DefaultPostLoadEventListener; +import org.hibernate.event.internal.DefaultPreFlushEventListener; import org.hibernate.event.internal.DefaultPreLoadEventListener; import org.hibernate.event.internal.DefaultRefreshEventListener; import org.hibernate.event.internal.DefaultReplicateEventListener; @@ -69,6 +70,7 @@ import static org.hibernate.event.spi.EventType.PRE_COLLECTION_REMOVE; import static org.hibernate.event.spi.EventType.PRE_COLLECTION_UPDATE; import static org.hibernate.event.spi.EventType.PRE_DELETE; +import static org.hibernate.event.spi.EventType.PRE_FLUSH; import static org.hibernate.event.spi.EventType.PRE_INSERT; import static org.hibernate.event.spi.EventType.PRE_LOAD; import static org.hibernate.event.spi.EventType.PRE_UPDATE; @@ -216,6 +218,9 @@ private void applyStandardListeners() { // auto-flush listeners prepareListeners( AUTO_FLUSH, new DefaultAutoFlushEventListener() ); + // pre-flush listeners + prepareListeners( PRE_FLUSH, new DefaultPreFlushEventListener() ); + // create listeners prepareListeners( PERSIST, new DefaultPersistEventListener() ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/service/spi/EventListenerGroups.java b/hibernate-core/src/main/java/org/hibernate/event/service/spi/EventListenerGroups.java index 53896f8e9211..1f7a9af2df13 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/service/spi/EventListenerGroups.java +++ b/hibernate-core/src/main/java/org/hibernate/event/service/spi/EventListenerGroups.java @@ -31,6 +31,7 @@ import org.hibernate.event.spi.PreCollectionRemoveEventListener; import org.hibernate.event.spi.PreCollectionUpdateEventListener; import org.hibernate.event.spi.PreDeleteEventListener; +import org.hibernate.event.spi.PreFlushEventListener; import org.hibernate.event.spi.PreInsertEventListener; import org.hibernate.event.spi.PreLoadEventListener; import org.hibernate.event.spi.PreUpdateEventListener; @@ -57,6 +58,7 @@ public final class EventListenerGroups { // All session events need to be iterated frequently; // CollectionAction and EventAction also need most of these very frequently: public final EventListenerGroup eventListenerGroup_AUTO_FLUSH; + public final EventListenerGroup eventListenerGroup_PRE_FLUSH; public final EventListenerGroup eventListenerGroup_CLEAR; public final EventListenerGroup eventListenerGroup_DELETE; public final EventListenerGroup eventListenerGroup_DIRTY_CHECK; @@ -103,6 +105,7 @@ public EventListenerGroups(ServiceRegistry serviceRegistry) { // Pre-compute all iterators on Event listeners: eventListenerGroup_AUTO_FLUSH = listeners( eventListenerRegistry, AUTO_FLUSH ); + eventListenerGroup_PRE_FLUSH = listeners( eventListenerRegistry, PRE_FLUSH ); eventListenerGroup_CLEAR = listeners( eventListenerRegistry, CLEAR ); eventListenerGroup_DELETE = listeners( eventListenerRegistry, DELETE ); eventListenerGroup_DIRTY_CHECK = listeners( eventListenerRegistry, DIRTY_CHECK ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/AutoFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/spi/AutoFlushEventListener.java index 26dc06aa397d..859620763a31 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/AutoFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/AutoFlushEventListener.java @@ -18,7 +18,4 @@ public interface AutoFlushEventListener { * @param event The auto-flush event to be handled. */ void onAutoFlush(AutoFlushEvent event) throws HibernateException; - - default void onAutoPreFlush(EventSource source) throws HibernateException { - } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java b/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java index 847f05636e95..79fa9fece972 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java @@ -39,6 +39,7 @@ public final class EventType { public static final EventType FLUSH = create( "flush", FlushEventListener.class ); public static final EventType AUTO_FLUSH = create( "auto-flush", AutoFlushEventListener.class ); + public static final EventType PRE_FLUSH = create( "pre-flush", PreFlushEventListener.class ); public static final EventType DIRTY_CHECK = create( "dirty-check", DirtyCheckEventListener.class ); public static final EventType FLUSH_ENTITY = create( "flush-entity", FlushEntityEventListener.class ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java new file mode 100644 index 000000000000..c4bc6c18ae14 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.event.spi; + +import org.hibernate.HibernateException; + +/** + * @author Gavin King + * @since 7.2 + */ +public interface PreFlushEventListener { + void onAutoPreFlush(AutoFlushEvent event) throws HibernateException; +} 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 08486989087a..75cf138c00c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -1437,13 +1437,14 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush() { + public void autoPreFlush(Set querySpaces) { checkOpen(); // do not auto-flush while outside a transaction if ( isTransactionInProgress() ) { - eventListenerGroups.eventListenerGroup_AUTO_FLUSH - .fireEventOnEachListener( this, - AutoFlushEventListener::onAutoPreFlush ); + final var autoFlushEvent = new AutoFlushEvent( querySpaces, false, this ); + eventListenerGroups.eventListenerGroup_PRE_FLUSH + .fireEventOnEachListener( autoFlushEvent, + PreFlushEventListener::onAutoPreFlush ); } } 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 7129a1bcd469..cad8a90b03b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -1325,7 +1325,7 @@ public void afterScrollOperation() { } @Override - public void autoPreFlush() { + public void autoPreFlush(Set querySpaces) { } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java index efddecc13f0d..a0486e7045f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java @@ -120,6 +120,11 @@ public boolean hasAnyMultiValuedBindings() { return false; } + @Override + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor factory) { + return false; + } + @Override public void visitBindings(BiConsumer, ? super QueryParameterBinding> action) { bindingMap.forEach( action ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java index 438d30648512..874c3942d63f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java @@ -19,6 +19,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.FilterImpl; import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.QueryParameter; import org.hibernate.query.spi.ParameterMetadataImplementor; import org.hibernate.query.spi.QueryParameterBinding; @@ -28,8 +29,10 @@ import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.engine.internal.CacheHelper.addBasicValueToCacheKey; +import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.internal.util.collections.CollectionHelper.linkedMapOfSize; import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * Manages the group of QueryParameterBinding for a particular query. @@ -178,6 +181,34 @@ public boolean hasAnyMultiValuedBindings() { return false; } + @Override + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor session) { + for ( var binding : parameterBindingMap.values() ) { + if ( binding.isMultiValued() ) { + for ( var value : binding.getBindValues() ) { + if ( isTransientEntityBinding( session, binding, value ) ) { + return true; + } + } + } + else { + if ( isTransientEntityBinding( session, binding, binding.getBindValue() ) ) { + return true; + } + } + } + return false; + } + + private static boolean isTransientEntityBinding( + SharedSessionContractImplementor session, QueryParameterBinding binding, Object value) { + return value != null && !isHibernateProxy( value ) && extractLazyInitializer( value ) == null + && binding.getBindType() instanceof EntityDomainType entityDomainType + && session.getFactory().getMappingMetamodel() + .getEntityDescriptor( entityDomainType.getHibernateEntityName() ) + .isTransient( value, session ) == Boolean.TRUE; + } + @Override public void visitBindings(BiConsumer, ? super QueryParameterBinding> action) { parameterBindingMap.forEach( action ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java index 7bbae586ecdd..e12406104935 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java @@ -76,6 +76,8 @@ default

QueryParameterBinding

getBinding(QueryParameter

parameter) { boolean hasAnyMultiValuedBindings(); + boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor factory); + /** * Generate a "memento" for these parameter bindings that can be used * in creating a {@link QueryKey} @@ -127,6 +129,11 @@ public boolean hasAnyMultiValuedBindings() { return false; } + @Override + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor factory) { + return false; + } + @Override public QueryKey.ParameterBindingsMemento generateQueryKeyMemento(SharedSessionContractImplementor session) { return NO_PARAMETER_BINDING_MEMENTO; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index 9f8abd3e090f..e035eb04176d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -88,7 +88,7 @@ public ConcreteSqmSelectQueryPlan( sqm.producesUniqueResults() && !containsCollectionFetches( queryOptions ) ? ListResultsConsumer.UniqueSemantic.NONE : ListResultsConsumer.UniqueSemantic.ALLOW; - executeQueryInterpreter = (resultsConsumer, executionContext, sqmInterpretation, jdbcParameterBindings) -> { + executeQueryInterpreter = (resultsConsumer, executionContext, sqmInterpretation, jdbcParameterBindings, skipPreFlush) -> { final var session = executionContext.getSession(); final var jdbcSelect = sqmInterpretation.jdbcOperation(); try { @@ -98,16 +98,14 @@ public ConcreteSqmSelectQueryPlan( JdbcParametersList.empty(), jdbcParameterBindings ); - session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), true ); - final var fetchExpression = - sqmInterpretation.statement().getQueryPart().getFetchClauseExpression(); + session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), skipPreFlush ); return session.getFactory().getJdbcServices().getJdbcSelectExecutor().executeQuery( jdbcSelect, jdbcParameterBindings, listInterpreterExecutionContext( hql, executionContext, jdbcSelect, subSelectFetchKeyHandler ), determineRowTransformer( sqm, resultType, tupleMetadata, executionContext.getQueryOptions() ), null, - resultCountEstimate( jdbcParameterBindings, fetchExpression ), + resultCountEstimate( sqmInterpretation, jdbcParameterBindings ), resultsConsumer ); } @@ -115,7 +113,7 @@ public ConcreteSqmSelectQueryPlan( domainParameterXref.clearExpansions(); } }; - this.listInterpreter = (unused, executionContext, sqmInterpretation, jdbcParameterBindings) -> { + this.listInterpreter = (unused, executionContext, sqmInterpretation, jdbcParameterBindings, skipPreFlush) -> { final var session = executionContext.getSession(); final var jdbcSelect = sqmInterpretation.jdbcOperation(); try { @@ -125,10 +123,7 @@ public ConcreteSqmSelectQueryPlan( JdbcParametersList.empty(), jdbcParameterBindings ); - session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), true ); - final var fetchExpression = - sqmInterpretation.statement().getQueryPart() - .getFetchClauseExpression(); + session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), skipPreFlush ); //noinspection unchecked return session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( jdbcSelect, @@ -137,7 +132,7 @@ public ConcreteSqmSelectQueryPlan( determineRowTransformer( sqm, resultType, tupleMetadata, executionContext.getQueryOptions() ), (Class) executionContext.getResultType(), uniqueSemantic, - resultCountEstimate( jdbcParameterBindings, fetchExpression ) + resultCountEstimate( sqmInterpretation, jdbcParameterBindings ) ); } finally { @@ -145,19 +140,18 @@ public ConcreteSqmSelectQueryPlan( } }; - this.scrollInterpreter = (scrollMode, executionContext, sqmInterpretation, jdbcParameterBindings) -> { + this.scrollInterpreter = (scrollMode, executionContext, sqmInterpretation, jdbcParameterBindings, skipPreFlush) -> { final var session = executionContext.getSession(); final var jdbcSelect = sqmInterpretation.jdbcOperation(); try { - session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), true ); - final var fetchExpression = sqmInterpretation.statement().getQueryPart().getFetchClauseExpression(); + session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), skipPreFlush ); return session.getFactory().getJdbcServices().getJdbcSelectExecutor().scroll( jdbcSelect, scrollMode, jdbcParameterBindings, new SqmJdbcExecutionContextAdapter( executionContext, jdbcSelect ), determineRowTransformer( sqm, resultType, tupleMetadata, executionContext.getQueryOptions() ), - resultCountEstimate( jdbcParameterBindings, fetchExpression ) + resultCountEstimate( sqmInterpretation, jdbcParameterBindings ) ); } finally { @@ -166,10 +160,15 @@ public ConcreteSqmSelectQueryPlan( }; } + private static int resultCountEstimate( + CacheableSqmInterpretation sqmInterpretation, + JdbcParameterBindings jdbcParameterBindings) { + return resultCountEstimate( jdbcParameterBindings, + sqmInterpretation.statement().getQueryPart().getFetchClauseExpression() ); + } + private static int resultCountEstimate(JdbcParameterBindings jdbcParameterBindings, Expression fetchExpression) { - return fetchExpression == null - ? -1 - : interpretIntExpression( fetchExpression, jdbcParameterBindings ); + return fetchExpression == null ? -1 : interpretIntExpression( fetchExpression, jdbcParameterBindings ); } protected static SqmJdbcExecutionContextAdapter listInterpreterExecutionContext( @@ -238,7 +237,7 @@ else if ( resultClass == null || resultClass == Object.class ) { throw new AssertionFailure( "No selections" ); } else { - final Class resultType = primitiveToWrapper( resultClass ); + final var resultType = primitiveToWrapper( resultClass ); return switch ( selections.size() ) { case 0 -> throw new AssertionFailure( "No selections" ); case 1 -> singleItemRowTransformer( sqm, tupleMetadata, selections.get( 0 ), resultType ); @@ -364,15 +363,22 @@ public ScrollableResultsImplementor performScroll(ScrollMode scrollMode, Doma } private T withCacheableSqmInterpretation(DomainQueryExecutionContext executionContext, X context, SqmInterpreter interpreter) { - // NOTE: VERY IMPORTANT - intentional double-lock checking - // The other option would be to leverage `java.util.concurrent.locks.ReadWriteLock` - // to protect access. However, synchronized is much simpler here. We will verify - // during throughput testing whether this is an issue and consider changes then + final var session = executionContext.getSession(); + final boolean preFlush = + executionContext.getQueryParameterBindings() + .hasAnyTransientEntityBindings( session ); + if ( preFlush ) { + session.autoPreFlush( null ); + } - CacheableSqmInterpretation localCopy = cacheableSqmInterpretation; - JdbcParameterBindings jdbcParameterBindings = null; + // IMPORTANT NOTE: Intentional double-lock checking + // Another solution would be to use ReadWriteLock + // to protect access. But synchronized is simpler here. + // We will verify during throughput testing whether + // this is an issue and consider changes then. - executionContext.getSession().autoPreFlush(); + var localCopy = cacheableSqmInterpretation; + JdbcParameterBindings jdbcParameterBindings = null; if ( localCopy == null ) { synchronized ( this ) { @@ -386,12 +392,13 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec else { // If the translation depends on parameter bindings or it isn't compatible with the current query options, // we have to rebuild the JdbcSelect, which is still better than having to translate from SQM to SQL AST again - if ( localCopy.jdbcOperation().dependsOnParameterBindings() ) { + final var jdbcSelect = localCopy.jdbcOperation(); + if ( jdbcSelect.dependsOnParameterBindings() ) { jdbcParameterBindings = createJdbcParameterBindings( localCopy, executionContext ); } // If the translation depends on the limit or lock options, we have to rebuild the JdbcSelect // We could avoid this by putting the lock options into the cache key - if ( !localCopy.jdbcOperation().isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { + if ( !jdbcSelect.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { final MutableObject mutableValue = new MutableObject<>(); localCopy = buildInterpretation( sqm, domainParameterXref, executionContext, mutableValue ); jdbcParameterBindings = mutableValue.get(); @@ -403,12 +410,13 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec else { // If the translation depends on parameter bindings or it isn't compatible with the current query options, // we have to rebuild the JdbcSelect, which is still better than having to translate from SQM to SQL AST again - if ( localCopy.jdbcOperation().dependsOnParameterBindings() ) { + final var jdbcSelect = localCopy.jdbcOperation(); + if ( jdbcSelect.dependsOnParameterBindings() ) { jdbcParameterBindings = createJdbcParameterBindings( localCopy, executionContext ); } // If the translation depends on the limit or lock options, we have to rebuild the JdbcSelect // We could avoid this by putting the lock options into the cache key - if ( !localCopy.jdbcOperation().isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { + if ( !jdbcSelect.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { final MutableObject mutableValue = new MutableObject<>(); localCopy = buildInterpretation( sqm, domainParameterXref, executionContext, mutableValue ); jdbcParameterBindings = mutableValue.get(); @@ -420,11 +428,13 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec jdbcParameterBindings = createJdbcParameterBindings( localCopy, executionContext ); } - return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings ); + return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings, preFlush ); } // For Hibernate Reactive - protected JdbcParameterBindings createJdbcParameterBindings(CacheableSqmInterpretation sqmInterpretation, DomainQueryExecutionContext executionContext) { + protected JdbcParameterBindings createJdbcParameterBindings( + CacheableSqmInterpretation sqmInterpretation, + DomainQueryExecutionContext executionContext) { return SqmUtil.createJdbcParameterBindings( executionContext.getQueryParameterBindings(), domainParameterXref, @@ -497,7 +507,8 @@ T interpret( X context, DomainQueryExecutionContext executionContext, CacheableSqmInterpretation sqmInterpretation, - JdbcParameterBindings jdbcParameterBindings); + JdbcParameterBindings jdbcParameterBindings, + boolean skipPreFlush); } private static class MySqmJdbcExecutionContextAdapter extends SqmJdbcExecutionContextAdapter { From 119cd8b00ae53d224e421c7795a4404118c7f121 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 13:32:50 +0100 Subject: [PATCH 5/7] introduce PreFlushEvent --- .../engine/spi/SessionDelegatorBaseImpl.java | 5 ++- .../spi/SharedSessionContractImplementor.java | 3 +- .../spi/SharedSessionDelegatorBaseImpl.java | 5 ++- .../DefaultAutoFlushEventListener.java | 4 +- .../DefaultPreFlushEventListener.java | 30 +++++++++++--- .../hibernate/event/spi/PreFlushEvent.java | 41 +++++++++++++++++++ .../event/spi/PreFlushEventListener.java | 6 ++- .../org/hibernate/internal/SessionImpl.java | 17 ++++---- .../internal/StatelessSessionImpl.java | 4 +- .../internal/ProcedureParamBindings.java | 4 +- .../query/spi/QueryParameterBindings.java | 6 +-- .../internal/ConcreteSqmSelectQueryPlan.java | 10 ++--- 12 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEvent.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index d0bce95054ac..492ecd4462c8 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -60,6 +60,7 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryProducerImplementor; import org.hibernate.query.sql.spi.NativeQueryImplementor; import org.hibernate.resource.jdbc.spi.JdbcSessionContext; @@ -418,8 +419,8 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush(Set querySpaces) { - delegate.autoPreFlush( querySpaces ); + public boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings) { + return delegate.autoPreFlushIfRequired( parameterBindings ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java index 3ae535a0f43f..f655ecf2bb73 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java @@ -29,6 +29,7 @@ import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryProducerImplementor; import org.hibernate.resource.jdbc.spi.JdbcSessionOwner; import org.hibernate.resource.transaction.spi.TransactionCoordinator; @@ -535,7 +536,7 @@ default boolean autoFlushIfRequired(Set querySpaces) { */ boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush); - void autoPreFlush(Set querySpaces); + boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings); /** * Check if there is a Hibernate or JTA transaction in progress and, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index db7eedf9d878..dd504209d509 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -40,6 +40,7 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryProducerImplementor; import org.hibernate.query.sql.spi.NativeQueryImplementor; import org.hibernate.resource.jdbc.spi.JdbcSessionContext; @@ -569,8 +570,8 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush(Set querySpaces) { - delegate.autoPreFlush( querySpaces ); + public boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings) { + return delegate.autoPreFlushIfRequired( parameterBindings ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java index 30ea842b27a4..278433fda8ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultAutoFlushEventListener.java @@ -81,12 +81,12 @@ public void onAutoFlush(AutoFlushEvent event) throws HibernateException { } } - static boolean flushIsReallyNeeded(AutoFlushEvent event, EventSource source) { + private static boolean flushIsReallyNeeded(AutoFlushEvent event, EventSource source) { return source.getHibernateFlushMode() == FlushMode.ALWAYS || source.getActionQueue().areTablesToBeUpdated( event.getQuerySpaces() ); } - static boolean flushMightBeNeeded(AutoFlushEvent event, EventSource source) { + private static boolean flushMightBeNeeded(AutoFlushEvent event, EventSource source) { return flushMightBeNeededForMode( event, source ) && nonEmpty( source ); } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java index 9f42d745fc31..fd5e3ccd1623 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java @@ -5,21 +5,22 @@ package org.hibernate.event.internal; import org.hibernate.HibernateException; -import org.hibernate.event.spi.AutoFlushEvent; +import org.hibernate.event.spi.EventSource; +import org.hibernate.event.spi.PreFlushEvent; import org.hibernate.event.spi.PreFlushEventListener; -import static org.hibernate.event.internal.DefaultAutoFlushEventListener.flushMightBeNeeded; - public class DefaultPreFlushEventListener extends AbstractFlushingEventListener implements PreFlushEventListener { + @Override - public void onAutoPreFlush(AutoFlushEvent event) throws HibernateException { + public void onAutoPreFlush(PreFlushEvent event) throws HibernateException { final var source = event.getEventSource(); final var eventListenerManager = source.getEventListenerManager(); eventListenerManager.prePartialFlushStart(); final var eventMonitor = source.getEventMonitor(); final var diagnosticEvent = eventMonitor.beginPrePartialFlush(); try { - if ( flushMightBeNeeded( event, source ) ) { + if ( preFlushMightBeNeeded( source ) + && event.getParameterBindings().hasAnyTransientEntityBindings( source ) ) { preFlush( source, source.getPersistenceContextInternal() ); } } @@ -28,4 +29,23 @@ public void onAutoPreFlush(AutoFlushEvent event) throws HibernateException { eventListenerManager.prePartialFlushEnd(); } } + + + private static boolean preFlushMightBeNeeded(EventSource source) { + return flushMightBeNeededForMode( source ) + && nonEmpty( source ); + } + + private static boolean flushMightBeNeededForMode(EventSource source) { + return switch ( source.getHibernateFlushMode() ) { + case ALWAYS, AUTO -> true; + case MANUAL, COMMIT -> false; + }; + } + + private static boolean nonEmpty(EventSource source) { + final var persistenceContext = source.getPersistenceContextInternal(); + return persistenceContext.getNumberOfManagedEntities() > 0 + || persistenceContext.getCollectionEntriesSize() > 0; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEvent.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEvent.java new file mode 100644 index 000000000000..61bb52bd2175 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEvent.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.event.spi; + +import org.hibernate.Incubating; +import org.hibernate.query.spi.QueryParameterBindings; + +/** + * An event that occurs just before arguments are bound to JDBC + * parameters during execution of HQL. Gives Hibernate a chance + * to persist any transient entities used as query parameter + * arguments. + * + * @author Gavin King + * @since 7.2 + */ +@Incubating +public class PreFlushEvent extends AbstractSessionEvent { + + private boolean preFlushRequired; + private final QueryParameterBindings parameterBindings; + + public PreFlushEvent(QueryParameterBindings parameterBindings, EventSource source) { + super( source ); + this.parameterBindings = parameterBindings; + } + + public QueryParameterBindings getParameterBindings() { + return parameterBindings; + } + + public boolean isPreFlushRequired() { + return preFlushRequired; + } + + public void setPreFlushRequired(boolean preFlushRequired) { + this.preFlushRequired = preFlushRequired; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java index c4bc6c18ae14..7f1145f143d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java @@ -5,11 +5,15 @@ package org.hibernate.event.spi; import org.hibernate.HibernateException; +import org.hibernate.Incubating; /** + * A listener for events of type {@link PreFlushEvent}. + * * @author Gavin King * @since 7.2 */ +@Incubating public interface PreFlushEventListener { - void onAutoPreFlush(AutoFlushEvent event) throws HibernateException; + void onAutoPreFlush(PreFlushEvent event) throws HibernateException; } 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 75cf138c00c7..158b9e711b5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -61,6 +61,7 @@ import org.hibernate.query.SelectionQuery; import org.hibernate.query.UnknownSqlResultSetMappingException; import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.resource.jdbc.spi.JdbcSessionOwner; import org.hibernate.resource.transaction.spi.TransactionCoordinator; import org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder; @@ -1437,15 +1438,17 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush(Set querySpaces) { + public boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings) { checkOpen(); - // do not auto-flush while outside a transaction - if ( isTransactionInProgress() ) { - final var autoFlushEvent = new AutoFlushEvent( querySpaces, false, this ); - eventListenerGroups.eventListenerGroup_PRE_FLUSH - .fireEventOnEachListener( autoFlushEvent, - PreFlushEventListener::onAutoPreFlush ); + if ( !isTransactionInProgress() ) { + // do not auto-flush while outside a transaction + return false; } + final var preFlushEvent = new PreFlushEvent( parameterBindings, this ); + eventListenerGroups.eventListenerGroup_PRE_FLUSH + .fireEventOnEachListener( preFlushEvent, + PreFlushEventListener::onAutoPreFlush ); + return preFlushEvent.isPreFlushRequired(); } @Override 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 cad8a90b03b4..ce7237e3000a 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -69,6 +69,7 @@ import org.hibernate.loader.internal.CacheLoadHelper; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.stat.spi.StatisticsImplementor; import java.util.List; @@ -1325,7 +1326,8 @@ public void afterScrollOperation() { } @Override - public void autoPreFlush(Set querySpaces) { + public boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings) { + return false; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java index a0486e7045f4..f53fb220006a 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java @@ -121,7 +121,7 @@ public boolean hasAnyMultiValuedBindings() { } @Override - public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor factory) { + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor session) { return false; } @@ -131,7 +131,7 @@ public void visitBindings(BiConsumer, ? super QueryPar } @Override - public QueryKey.ParameterBindingsMemento generateQueryKeyMemento(SharedSessionContractImplementor persistenceContext) { + public QueryKey.ParameterBindingsMemento generateQueryKeyMemento(SharedSessionContractImplementor session) { return NO_PARAMETER_BINDING_MEMENTO; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java index e12406104935..782eecfc15ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryParameterBindings.java @@ -76,13 +76,13 @@ default

QueryParameterBinding

getBinding(QueryParameter

parameter) { boolean hasAnyMultiValuedBindings(); - boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor factory); + boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor session); /** * Generate a "memento" for these parameter bindings that can be used * in creating a {@link QueryKey} */ - QueryKey.ParameterBindingsMemento generateQueryKeyMemento(SharedSessionContractImplementor persistenceContext); + QueryKey.ParameterBindingsMemento generateQueryKeyMemento(SharedSessionContractImplementor session); void visitBindings(BiConsumer, ? super QueryParameterBinding> action); @@ -130,7 +130,7 @@ public boolean hasAnyMultiValuedBindings() { } @Override - public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor factory) { + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor session) { return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index e035eb04176d..989c517f27d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -364,12 +364,8 @@ public ScrollableResultsImplementor performScroll(ScrollMode scrollMode, Doma private T withCacheableSqmInterpretation(DomainQueryExecutionContext executionContext, X context, SqmInterpreter interpreter) { final var session = executionContext.getSession(); - final boolean preFlush = - executionContext.getQueryParameterBindings() - .hasAnyTransientEntityBindings( session ); - if ( preFlush ) { - session.autoPreFlush( null ); - } + + final boolean preFlushed = session.autoPreFlushIfRequired( executionContext.getQueryParameterBindings() ); // IMPORTANT NOTE: Intentional double-lock checking // Another solution would be to use ReadWriteLock @@ -428,7 +424,7 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec jdbcParameterBindings = createJdbcParameterBindings( localCopy, executionContext ); } - return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings, preFlush ); + return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings, preFlushed ); } // For Hibernate Reactive From 77a5ed504a68bab8c2c067027a8868a0a347cdd9 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 13:53:23 +0100 Subject: [PATCH 6/7] add new class to StaticCLassLists --- .../java/org/hibernate/graalvm/internal/StaticClassLists.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hibernate-graalvm/src/main/java/org/hibernate/graalvm/internal/StaticClassLists.java b/hibernate-graalvm/src/main/java/org/hibernate/graalvm/internal/StaticClassLists.java index a90b752193cf..8f6625a0278a 100644 --- a/hibernate-graalvm/src/main/java/org/hibernate/graalvm/internal/StaticClassLists.java +++ b/hibernate-graalvm/src/main/java/org/hibernate/graalvm/internal/StaticClassLists.java @@ -51,6 +51,7 @@ public static Class[] typesNeedingArrayCopy() { org.hibernate.event.spi.ReplicateEventListener[].class, org.hibernate.event.spi.FlushEventListener[].class, org.hibernate.event.spi.AutoFlushEventListener[].class, + org.hibernate.event.spi.PreFlushEventListener[].class, org.hibernate.event.spi.DirtyCheckEventListener[].class, org.hibernate.event.spi.FlushEntityEventListener[].class, org.hibernate.event.spi.ClearEventListener[].class, From 9d189c16bdc783cd50b707c87284f1d0c1104096 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 19 Nov 2025 14:44:35 +0100 Subject: [PATCH 7/7] remove unnecessary call Co-authored-by: Marco Belladelli --- .../hibernate/query/internal/QueryParameterBindingsImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java index 874c3942d63f..281a0b5de19e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java @@ -32,7 +32,6 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.internal.util.collections.CollectionHelper.linkedMapOfSize; import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; -import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * Manages the group of QueryParameterBinding for a particular query. @@ -202,7 +201,7 @@ public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor se private static boolean isTransientEntityBinding( SharedSessionContractImplementor session, QueryParameterBinding binding, Object value) { - return value != null && !isHibernateProxy( value ) && extractLazyInitializer( value ) == null + return value != null && !isHibernateProxy( value ) && binding.getBindType() instanceof EntityDomainType entityDomainType && session.getFactory().getMappingMetamodel() .getEntityDescriptor( entityDomainType.getHibernateEntityName() )