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) { 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..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() { - delegate.autoPreFlush(); + 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 958e71319ab4..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(); + 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 4f7f1951c05f..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() { - delegate.autoPreFlush(); + public boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings) { + return delegate.autoPreFlushIfRequired( parameterBindings ); } @Override 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 { 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..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 @@ -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(); @@ -82,32 +81,30 @@ 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( source ) ) { - preFlush( source, source.getPersistenceContextInternal() ); - } - } - finally { - eventMonitor.completePrePartialFlush( diagnosticEvent, source ); - eventListenerManager.prePartialFlushEnd(); - } - } - - private boolean flushIsReallyNeeded(AutoFlushEvent event, final EventSource source) { + private static boolean flushIsReallyNeeded(AutoFlushEvent event, EventSource source) { return source.getHibernateFlushMode() == FlushMode.ALWAYS || source.getActionQueue().areTablesToBeUpdated( event.getQuerySpaces() ); } - private boolean flushMightBeNeeded(final EventSource source) { + private static 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 -> { + final var querySpaces = event.getQuerySpaces(); + yield querySpaces == null || !querySpaces.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; } } 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..fd5e3ccd1623 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultPreFlushEventListener.java @@ -0,0 +1,51 @@ +/* + * 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.EventSource; +import org.hibernate.event.spi.PreFlushEvent; +import org.hibernate.event.spi.PreFlushEventListener; + +public class DefaultPreFlushEventListener extends AbstractFlushingEventListener implements PreFlushEventListener { + + @Override + 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 ( preFlushMightBeNeeded( source ) + && event.getParameterBindings().hasAnyTransientEntityBindings( source ) ) { + preFlush( source, source.getPersistenceContextInternal() ); + } + } + finally { + eventMonitor.completePrePartialFlush( diagnosticEvent, source ); + 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/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/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 new file mode 100644 index 000000000000..7f1145f143d8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/PreFlushEventListener.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +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(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 08486989087a..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,14 +1438,17 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush } @Override - public void autoPreFlush() { + public boolean autoPreFlushIfRequired(QueryParameterBindings parameterBindings) { checkOpen(); - // do not auto-flush while outside a transaction - if ( isTransactionInProgress() ) { - eventListenerGroups.eventListenerGroup_AUTO_FLUSH - .fireEventOnEachListener( this, - AutoFlushEventListener::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 7129a1bcd469..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() { + 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 efddecc13f0d..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 @@ -120,13 +120,18 @@ public boolean hasAnyMultiValuedBindings() { return false; } + @Override + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor session) { + return false; + } + @Override public void visitBindings(BiConsumer, ? super QueryParameterBinding> action) { bindingMap.forEach( action ); } @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/internal/QueryParameterBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java index 438d30648512..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 @@ -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,6 +29,7 @@ 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; @@ -178,6 +180,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 ) + && 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..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,11 +76,13 @@ default

QueryParameterBinding

getBinding(QueryParameter

parameter) { boolean hasAnyMultiValuedBindings(); + 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); @@ -127,6 +129,11 @@ public boolean hasAnyMultiValuedBindings() { return false; } + @Override + public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor session) { + 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..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 @@ -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,18 @@ 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(); - CacheableSqmInterpretation localCopy = cacheableSqmInterpretation; - JdbcParameterBindings jdbcParameterBindings = null; + final boolean preFlushed = session.autoPreFlushIfRequired( executionContext.getQueryParameterBindings() ); + + // 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 +388,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 +406,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 +424,13 @@ private T withCacheableSqmInterpretation(DomainQueryExecutionContext exec jdbcParameterBindings = createJdbcParameterBindings( localCopy, executionContext ); } - return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings ); + return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings, preFlushed ); } // 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 +503,8 @@ T interpret( X context, DomainQueryExecutionContext executionContext, CacheableSqmInterpretation sqmInterpretation, - JdbcParameterBindings jdbcParameterBindings); + JdbcParameterBindings jdbcParameterBindings, + boolean skipPreFlush); } private static class MySqmJdbcExecutionContextAdapter extends SqmJdbcExecutionContextAdapter { 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,