From bfe244fd17e937bc1a35d16d88d9bea67c44cbef Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 9 Apr 2023 20:05:10 +0200 Subject: [PATCH 1/3] update DefaultReactiveListeners --- .../impl/ReactiveCollectionRemoveAction.java | 17 + .../impl/ReactiveEntityDeleteAction.java | 127 ++-- ...AbstractReactiveFlushingEventListener.java | 71 +- .../AbstractReactiveSaveEventListener.java | 93 ++- ...DefaultReactiveAutoFlushEventListener.java | 2 +- .../DefaultReactiveDeleteEventListener.java | 338 +++++++--- ...faultReactiveFlushEntityEventListener.java | 628 +++++++++--------- .../DefaultReactiveFlushEventListener.java | 17 +- ...tiveInitializeCollectionEventListener.java | 68 +- .../DefaultReactiveLoadEventListener.java | 151 +++-- .../DefaultReactiveLockEventListener.java | 67 +- .../DefaultReactiveMergeEventListener.java | 241 ++++--- .../DefaultReactivePersistEventListener.java | 121 ++-- .../DefaultReactivePostLoadEventListener.java | 28 +- .../DefaultReactiveRefreshEventListener.java | 115 ++-- ...ReactiveResolveNaturalIdEventListener.java | 32 +- .../session/impl/ReactiveSessionImpl.java | 4 +- 17 files changed, 1182 insertions(+), 938 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveCollectionRemoveAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveCollectionRemoveAction.java index 9742a5e04..525c8d92f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveCollectionRemoveAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveCollectionRemoveAction.java @@ -49,6 +49,23 @@ public ReactiveCollectionRemoveAction( this.affectedOwner = session.getPersistenceContextInternal().getLoadedCollectionOwnerOrNull( collection ); } + /** + * Removes a persistent collection for an unloaded proxy. + * + * Use this constructor when the owning entity is has not been loaded. + * @param persister The collection's persister + * @param id The collection key + * @param session The session + */ + public ReactiveCollectionRemoveAction( + final CollectionPersister persister, + final Object id, + final EventSource session) { + super( persister, null, id, session ); + emptySnapshot = false; + affectedOwner = null; + } + @Override public CompletionStage reactiveExecute() { final Object key = getKey(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java index 78978dade..8d036eb56 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java @@ -13,6 +13,7 @@ import org.hibernate.action.internal.EntityDeleteAction; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; @@ -43,66 +44,45 @@ public ReactiveEntityDeleteAction( super( id, state, version, instance, persister, isCascadeDeleteEnabled, session ); } + public ReactiveEntityDeleteAction(Object id, EntityPersister persister, EventSource session) { + super( id, persister, session ); + } + @Override public void execute() throws HibernateException { throw LOG.nonReactiveMethodCall( "reactiveExecute" ); } + private boolean isInstanceLoaded() { + // A null instance signals that we're deleting an unloaded proxy. + return getInstance() != null; + } + @Override public CompletionStage reactiveExecute() throws HibernateException { final Object id = getId(); + final Object version = getCurrentVersion(); final EntityPersister persister = getPersister(); final SharedSessionContractImplementor session = getSession(); final Object instance = getInstance(); - final boolean veto = preDelete(); + final boolean veto = isInstanceLoaded() && preDelete(); - Object version = getVersion(); - if ( persister.isVersionPropertyGenerated() ) { - // we need to grab the version value from the entity, otherwise - // we have issues with generated-version entities that may have - // multiple actions queued during the same flush - version = persister.getVersion( instance ); - } + final Object ck = lockCacheItem(); - final Object ck; - if ( persister.canWriteToCache() ) { - final EntityDataAccess cache = persister.getCacheAccessStrategy(); - ck = cache.generateCacheKey( id, persister, session.getFactory(), session.getTenantIdentifier() ); - setLock( cache.lockItem( session, ck, version ) ); - } - else { - ck = null; - } - - CompletionStage deleteStep = !isCascadeDeleteEnabled() && !veto + final CompletionStage deleteStep = !isCascadeDeleteEnabled() && !veto ? ( (ReactiveEntityPersister) persister ).deleteReactive( id, version, instance, session ) : voidFuture(); return deleteStep.thenAccept( v -> { - //postDelete: - // After actually deleting a row, record the fact that the instance no longer - // exists on the database (needed for identity-column key generation), and - // remove it from the session cache - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - final EntityEntry entry = persistenceContext.removeEntry( instance ); - if ( entry == null ) { - throw new AssertionFailure( "possible non-threadsafe access to session" ); + if ( isInstanceLoaded() ) { + postDeleteLoaded( id, persister, session, instance, ck ); } - entry.postDelete(); - - persistenceContext.removeEntity( entry.getEntityKey() ); - persistenceContext.removeProxy( entry.getEntityKey() ); - - if ( persister.canWriteToCache() ) { - persister.getCacheAccessStrategy().remove( session, ck ); + else { + // we're deleting an unloaded proxy + postDeleteUnloaded( id, persister, session, ck ); } - persistenceContext.getNaturalIdResolutions() - .removeSharedResolution( id, getNaturalIdValues(), persister ); - - postDelete(); - final StatisticsImplementor statistics = getSession().getFactory().getStatistics(); if ( statistics.isStatisticsEnabled() && !veto ) { statistics.deleteEntity( getPersister().getEntityName() ); @@ -110,4 +90,73 @@ public CompletionStage reactiveExecute() throws HibernateException { } ); } + //TODO: copy/paste from superclass (make it protected!) + private Object getCurrentVersion() { + return getPersister().isVersionPropertyGenerated() + // skip if we're deleting an unloaded proxy, no need for the version + && isInstanceLoaded() + // we need to grab the version value from the entity, otherwise + // we have issues with generated-version entities that may have + // multiple actions queued during the same flush + ? getPersister().getVersion( getInstance() ) + : getVersion(); + } + + //TODO: copy/paste of postDeleteLoaded() from superclass (make it protected!) + private void postDeleteLoaded( + Object id, + EntityPersister persister, + SharedSessionContractImplementor session, + Object instance, + Object ck) { + // After actually deleting a row, record the fact that the instance no longer + // exists on the database (needed for identity-column key generation), and + // remove it from the session cache + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + final EntityEntry entry = persistenceContext.removeEntry(instance); + if ( entry == null ) { + throw new AssertionFailure( "possible non-threadsafe access to session" ); + } + entry.postDelete(); + final EntityKey key = entry.getEntityKey(); + persistenceContext.removeEntity( key ); + persistenceContext.removeProxy( key ); + removeCacheItem( ck ); + persistenceContext.getNaturalIdResolutions().removeSharedResolution( id, getNaturalIdValues(), persister ); + postDelete(); + } + + //TODO: copy/paste of postDeleteUnloaded() from superclass (make it protected!) + private void postDeleteUnloaded(Object id, EntityPersister persister, SharedSessionContractImplementor session, Object ck) { + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + final EntityKey key = session.generateEntityKey( id, persister ); + if ( !persistenceContext.containsDeletedUnloadedEntityKey( key ) ) { + throw new AssertionFailure( "deleted proxy should be for an unloaded entity: " + key ); + } + persistenceContext.removeProxy( key ); + removeCacheItem( ck ); + } + + //TODO: copy/paste from superclass (make it protected!) + private Object lockCacheItem() { + final EntityPersister persister = getPersister(); + if ( persister.canWriteToCache() ) { + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + final SharedSessionContractImplementor session = getSession(); + final Object ck = cache.generateCacheKey( getId(), persister, session.getFactory(), session.getTenantIdentifier() ); + setLock( cache.lockItem( session, ck, getCurrentVersion() ) ); + return ck; + } + else { + return null; + } + } + + //TODO: copy/paste from superclass (make it protected!) + private void removeCacheItem(Object ck) { + final EntityPersister persister = getPersister(); + if ( persister.canWriteToCache() ) { + persister.getCacheAccessStrategy().remove( getSession(), ck); + } + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveFlushingEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveFlushingEventListener.java index 6888fd7ba..c178203e8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveFlushingEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveFlushingEventListener.java @@ -6,7 +6,6 @@ package org.hibernate.reactive.event.impl; import static org.hibernate.reactive.util.impl.CompletionStages.loop; -import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; import java.lang.invoke.MethodHandles; import java.util.Map; @@ -26,6 +25,7 @@ import org.hibernate.event.spi.FlushEntityEventListener; import org.hibernate.event.spi.FlushEvent; import org.hibernate.event.spi.PersistContext; +import org.hibernate.internal.util.EntityPrinter; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.engine.ReactiveActionQueue; import org.hibernate.reactive.engine.impl.Cascade; @@ -54,15 +54,13 @@ protected CompletionStage performExecutions(EventSource session) { // during-flush callbacks more leniency in regards to initializing proxies and // lazy collections during their processing. // For more information, see HHH-2763 - return voidFuture() - .thenCompose( v -> { - session.getJdbcCoordinator().flushBeginning(); - session.getPersistenceContext().setFlushing( true ); - // we need to lock the collection caches before executing entity inserts/updates in order to - // account for bi-directional associations - actionQueue( session ).prepareActions(); - return actionQueue( session ).executeActions(); - } ) + session.getJdbcCoordinator().flushBeginning(); + session.getPersistenceContext().setFlushing( true ); + // we need to lock the collection caches before executing entity inserts/updates + // in order to account for bidirectional associations + final ReactiveActionQueue actionQueue = actionQueue(session); + actionQueue.prepareActions(); + return actionQueue.executeActions() .whenComplete( (v, x) -> { session.getPersistenceContext().setFlushing( false ); session.getJdbcCoordinator().flushEnding(); @@ -85,12 +83,12 @@ protected CompletionStage flushEverythingToExecutions(FlushEvent event) th LOG.trace( "Flushing session" ); - EventSource session = event.getSession(); + final EventSource session = event.getSession(); final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); session.getInterceptor().preFlush( persistenceContext.managedEntitiesIterator() ); - return prepareEntityFlushes(session, persistenceContext) + return prepareEntityFlushes( session, persistenceContext ) .thenAccept( v -> { // we could move this inside if we wanted to // tolerate collection initializations during @@ -99,21 +97,46 @@ protected CompletionStage flushEverythingToExecutions(FlushEvent event) th // now, any collections that are initialized // inside this block do not get updated - they // are ignored until the next flush - persistenceContext.setFlushing(true); + persistenceContext.setFlushing( true ); try { - int entityCount = flushEntities(event, persistenceContext); - int collectionCount = flushCollections(session, persistenceContext); + int entityCount = flushEntities( event, persistenceContext ); + int collectionCount = flushCollections( session, persistenceContext ); event.setNumberOfEntitiesProcessed(entityCount); event.setNumberOfCollectionsProcessed(collectionCount); } finally { - persistenceContext.setFlushing(false); + persistenceContext.setFlushing( false ); } + + //some statistics + logFlushResults( event ); } ); + } - //some statistics -// logFlushResults( event ); + protected void logFlushResults(FlushEvent event) { + if ( !LOG.isDebugEnabled() ) { + return; + } + final EventSource session = event.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + LOG.debugf( + "Flushed: %s insertions, %s updates, %s deletions to %s objects", + session.getActionQueue().numberOfInsertions(), + session.getActionQueue().numberOfUpdates(), + session.getActionQueue().numberOfDeletions(), + persistenceContext.getNumberOfManagedEntities() + ); + LOG.debugf( + "Flushed: %s (re)creations, %s updates, %s removals to %s collections", + session.getActionQueue().numberOfCollectionCreations(), + session.getActionQueue().numberOfCollectionUpdates(), + session.getActionQueue().numberOfCollectionRemovals(), + persistenceContext.getCollectionEntriesSize() + ); + new EntityPrinter( session.getFactory() ).toString( + persistenceContext.getEntitiesByKey().entrySet() + ); } /** @@ -125,9 +148,9 @@ private CompletionStage prepareEntityFlushes(EventSource session, Persiste LOG.debug( "Processing flush-time cascades" ); - PersistContext context = PersistContext.create(); + final PersistContext context = PersistContext.create(); //safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap - Map.Entry[] entries = persistenceContext.reentrantSafeEntityEntries(); + final Map.Entry[] entries = persistenceContext.reentrantSafeEntityEntries(); return loop( entries, index -> flushable( entries[index].getValue() ), @@ -135,7 +158,7 @@ private CompletionStage prepareEntityFlushes(EventSource session, Persiste } private static boolean flushable(EntityEntry entry) { - Status status = entry.getStatus(); + final Status status = entry.getStatus(); return status == Status.MANAGED || status == Status.SAVING || status == Status.READ_ONLY; @@ -183,8 +206,8 @@ private int flushEntities(final FlushEvent event, final PersistenceContext persi // Update the status of the object and if necessary, schedule an update - EntityEntry entry = me.getValue(); - Status status = entry.getStatus(); + final EntityEntry entry = me.getValue(); + final Status status = entry.getStatus(); if ( status != Status.LOADING && status != Status.GONE ) { final FlushEntityEvent entityEvent = new FlushEntityEvent( source, me.getKey(), entry ); @@ -316,7 +339,7 @@ protected void postFlush(SessionImplementor session) throws HibernateException { } else { //otherwise recreate the mapping between the collection and its key - CollectionKey collectionKey = new CollectionKey( + final CollectionKey collectionKey = new CollectionKey( collectionEntry.getLoadedPersister(), collectionEntry.getLoadedKey() ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java index 3f253a5c2..2bf325e26 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java @@ -14,7 +14,6 @@ import org.hibernate.action.internal.AbstractEntityInsertAction; import org.hibernate.action.internal.EntityIdentityInsertAction; import org.hibernate.engine.internal.CascadePoint; -import org.hibernate.engine.internal.Versioning; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityEntryExtraState; import org.hibernate.engine.spi.EntityKey; @@ -30,6 +29,7 @@ import org.hibernate.jpa.event.spi.CallbackRegistry; import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.engine.ReactiveActionQueue; import org.hibernate.reactive.engine.impl.Cascade; import org.hibernate.reactive.engine.impl.CascadingAction; import org.hibernate.reactive.engine.impl.ReactiveEntityIdentityInsertAction; @@ -43,6 +43,8 @@ import org.hibernate.type.TypeHelper; import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker; +import static org.hibernate.engine.internal.Versioning.getVersion; +import static org.hibernate.engine.internal.Versioning.seedVersion; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.id.IdentifierGeneratorHelper.SHORT_CIRCUIT_INDICATOR; import static org.hibernate.pretty.MessageHelper.infoString; @@ -124,7 +126,7 @@ protected CompletionStage reactiveSaveWithGeneratedId( processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes ); final EntityPersister persister = source.getEntityPersister( entityName, entity ); - Generator generator = persister.getGenerator(); + final Generator generator = persister.getGenerator(); if ( !generator.generatedOnExecution() ) { if ( generator instanceof ReactiveIdentifierGenerator ) { return ( (ReactiveIdentifierGenerator) generator ) @@ -135,8 +137,9 @@ protected CompletionStage reactiveSaveWithGeneratedId( final Object generatedId = ( (BeforeExecutionGenerator) generator ).generate( source, entity, null, INSERT ); return performSaveWithId( entity, context, source, persister, generator, generatedId ); } - - return reactivePerformSave( entity, null, persister, true, context, source, requiresImmediateIdAccess ); + else { + return reactivePerformSave( entity, null, persister, true, context, source, requiresImmediateIdAccess ); + } } private CompletionStage performSaveWithId( @@ -207,31 +210,32 @@ protected CompletionStage reactivePerformSave( } private CompletionStage entityKey(Object entity, Object id, EntityPersister persister, boolean useIdentityColumn, EventSource source) { - if ( useIdentityColumn ) { - return nullFuture(); - } - return generateEntityKey( id, persister, source ) - .thenApply( generatedKey -> { - persister.setIdentifier( entity, id, source ); - return generatedKey; - } ); + return useIdentityColumn + ? nullFuture() + : generateEntityKey( id, persister, source ) + .thenApply( generatedKey -> { + persister.setIdentifier( entity, id, source ); + return generatedKey; + } ); } private CompletionStage generateEntityKey(Object id, EntityPersister persister, EventSource source) { final EntityKey key = source.generateEntityKey( id, persister ); final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); - Object old = persistenceContext.getEntity( key ); + final Object old = persistenceContext.getEntity( key ); if ( old != null ) { if ( persistenceContext.getEntry( old ).getStatus() == Status.DELETED ) { return source.unwrap( ReactiveSession.class ) .reactiveForceFlush( persistenceContext.getEntry( old ) ) .thenApply( v -> key ); } - - return failedFuture( new NonUniqueObjectException( id, persister.getEntityName() ) ); + else { + return failedFuture( new NonUniqueObjectException( id, persister.getEntityName() ) ); + } + } + else { + return completedFuture( key ); } - - return completedFuture( key ); } /** @@ -259,16 +263,16 @@ protected CompletionStage reactivePerformSaveOrReplicate( EventSource source, boolean requiresImmediateIdAccess) { - Object id = key == null ? null : key.getIdentifier(); + final Object id = key == null ? null : key.getIdentifier(); - boolean inTransaction = source.isTransactionInProgress(); - boolean shouldDelayIdentityInserts = !inTransaction && !requiresImmediateIdAccess; + final boolean inTransaction = source.isTransactionInProgress(); + final boolean shouldDelayIdentityInserts = !inTransaction && !requiresImmediateIdAccess; final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); // Put a placeholder in entries, so we don't recurse back and try to save() the // same object again. QUESTION: should this be done before onSave() is called? // likewise, should it be done before onUpdate()? - EntityEntry original = persistenceContext.addEntry( + final EntityEntry original = persistenceContext.addEntry( entity, Status.SAVING, null, @@ -297,10 +301,9 @@ protected CompletionStage reactivePerformSaveOrReplicate( .thenCompose( insert -> cascadeAfterSave( source, persister, entity, context ) .thenAccept( unused -> { final Object finalId = handleGeneratedId( useIdentityColumn, id, insert ); - EntityEntry newEntry = persistenceContext.getEntry( entity ); - + final EntityEntry newEntry = persistenceContext.getEntry( entity ); if ( newEntry != original ) { - EntityEntryExtraState extraState = newEntry.getExtraState( EntityEntryExtraState.class ); + final EntityEntryExtraState extraState = newEntry.getExtraState( EntityEntryExtraState.class ); if ( extraState == null ) { newEntry.addExtraState( original.getExtraState( EntityEntryExtraState.class ) ); } @@ -312,7 +315,7 @@ protected CompletionStage reactivePerformSaveOrReplicate( private static Object handleGeneratedId(boolean useIdentityColumn, Object id, AbstractEntityInsertAction insert) { if ( useIdentityColumn && insert.isEarlyInsert() ) { if ( insert instanceof EntityIdentityInsertAction ) { - Object generatedId = ( (EntityIdentityInsertAction) insert ).getGeneratedId(); + final Object generatedId = ( (EntityIdentityInsertAction) insert ).getGeneratedId(); insert.handleNaturalIdPostSaveNotifications( generatedId ); return generatedId; } @@ -320,13 +323,14 @@ private static Object handleGeneratedId(boolean useIdentityColumn, Object id, Ab "Insert should be using an identity column, but action is of unexpected type: " + insert.getClass() .getName() ); } - - return id; + else { + return id; + } } private Object[] cloneAndSubstituteValues(Object entity, EntityPersister persister, C context, EventSource source, Object id) { - Object[] values = persister.getPropertyValuesToInsert( entity, getMergeMap(context), source ); - Type[] types = persister.getPropertyTypes(); + final Object[] values = persister.getPropertyValuesToInsert( entity, getMergeMap( context ), source ); + final Type[] types = persister.getPropertyTypes(); boolean substitute = substituteValuesIfNecessary( entity, id, values, persister, source ); if ( persister.hasCollections() ) { @@ -359,31 +363,25 @@ private CompletionStage addInsertAction( boolean useIdentityColumn, EventSource source, boolean shouldDelayIdentityInserts) { + final ReactiveActionQueue actionQueue = source.unwrap(ReactiveSession.class).getReactiveActionQueue(); if ( useIdentityColumn ) { - ReactiveEntityIdentityInsertAction insert = new ReactiveEntityIdentityInsertAction( + final ReactiveEntityIdentityInsertAction insert = new ReactiveEntityIdentityInsertAction( values, entity, persister, false, source, shouldDelayIdentityInserts ); - return source.unwrap(ReactiveSession.class) - .getReactiveActionQueue() - .addAction( insert ) - .thenApply( v -> insert ); + return actionQueue.addAction( insert ).thenApply( v -> insert ); } else { - Object version = Versioning.getVersion( values, persister ); - ReactiveEntityRegularInsertAction insert = new ReactiveEntityRegularInsertAction( - id, values, entity, version, persister, false, source + final ReactiveEntityRegularInsertAction insert = new ReactiveEntityRegularInsertAction( + id, values, entity, getVersion( values, persister ), persister, false, source ); - return source.unwrap(ReactiveSession.class) - .getReactiveActionQueue() - .addAction( insert ) - .thenApply( v -> insert ); + return actionQueue.addAction( insert ).thenApply( v -> insert ); } } /** * Handles the calls needed to perform pre-save cascades for the given entity. * - * @param source The session from whcih the save event originated. + * @param source The session from which the save event originated. * @param persister The entity's persister instance. * @param entity The entity to be saved. * @param context Generally cascade-specific data @@ -453,12 +451,7 @@ protected boolean substituteValuesIfNecessary( //keep the existing version number in the case of replicate! if ( persister.isVersioned() ) { - substitute = Versioning.seedVersion( - values, - persister.getVersionProperty(), - persister.getVersionMapping(), - source - ) || substitute; + substitute = seedVersion( entity, values, persister, source ) || substitute; } return substitute; } @@ -469,8 +462,8 @@ protected boolean visitCollectionsBeforeSave( Object[] values, Type[] types, EventSource source) { - WrapVisitor visitor = new WrapVisitor( entity, id, source ); - // substitutes into values by side-effect + final WrapVisitor visitor = new WrapVisitor( entity, id, source ); + // substitutes into values by side effect visitor.processEntityPropertyValues( values, types ); return visitor.isSubstitutionRequired(); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveAutoFlushEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveAutoFlushEventListener.java index 46e0cd609..ce6c12e65 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveAutoFlushEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveAutoFlushEventListener.java @@ -81,7 +81,7 @@ private ReactiveActionQueue reactiveActionQueue(AutoFlushEvent event) { private boolean flushIsReallyNeeded(AutoFlushEvent event, final EventSource source) { return source.getHibernateFlushMode() == FlushMode.ALWAYS - || reactiveActionQueue( source ).areTablesToBeUpdated( event.getQuerySpaces() ); + || reactiveActionQueue( source ).areTablesToBeUpdated( event.getQuerySpaces() ); } private ReactiveActionQueue reactiveActionQueue(EventSource source) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java index 971c3b722..1d8a03951 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java @@ -15,6 +15,8 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.TransientObjectException; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.engine.internal.CascadePoint; import org.hibernate.engine.internal.Nullability; import org.hibernate.engine.spi.EntityEntry; @@ -22,24 +24,36 @@ import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.Status; import org.hibernate.event.internal.OnUpdateVisitor; +import org.hibernate.event.internal.PostDeleteEventListenerStandardImpl; import org.hibernate.event.service.spi.JpaBootstrapSensitive; import org.hibernate.event.spi.DeleteContext; import org.hibernate.event.spi.DeleteEvent; import org.hibernate.event.spi.DeleteEventListener; import org.hibernate.event.spi.EventSource; +import org.hibernate.internal.EmptyInterceptor; +import org.hibernate.internal.FastSessionServices; import org.hibernate.jpa.event.spi.CallbackRegistry; import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; +import org.hibernate.jpa.event.spi.CallbackType; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl; +import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.LazyInitializer; import org.hibernate.reactive.engine.ReactiveActionQueue; import org.hibernate.reactive.engine.impl.Cascade; import org.hibernate.reactive.engine.impl.CascadingActions; import org.hibernate.reactive.engine.impl.ForeignKeys; +import org.hibernate.reactive.engine.impl.ReactiveCollectionRemoveAction; import org.hibernate.reactive.engine.impl.ReactiveEntityDeleteAction; import org.hibernate.reactive.engine.impl.ReactiveOrphanRemovalAction; import org.hibernate.reactive.event.ReactiveDeleteEventListener; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.type.CollectionType; +import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.type.TypeHelper; @@ -107,14 +121,73 @@ public CompletionStage reactiveOnDelete(DeleteEvent event) throws Hibernat */ @Override public CompletionStage reactiveOnDelete(DeleteEvent event, DeleteContext transientEntities) throws HibernateException { - EventSource source = event.getSession(); - if ( event.getObject() instanceof CompletionStage ) { - CompletionStage objectStage = (CompletionStage) event.getObject(); - return objectStage - .thenCompose( objectEvent -> fetchAndDelete( event, transientEntities, source, objectEvent ) ); + if ( optimizeUnloadedDelete( event ) ) { + return voidFuture(); } + else { + final EventSource source = event.getSession(); + Object object = event.getObject(); + if ( object instanceof CompletionStage ) { + final CompletionStage objectStage = (CompletionStage) object; + return objectStage.thenCompose( objectEvent -> fetchAndDelete( event, transientEntities, source, objectEvent ) ); + } + else { + return fetchAndDelete( event, transientEntities, source, object); + } + } + } + + private boolean optimizeUnloadedDelete(DeleteEvent event) { + final Object object = event.getObject(); + final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( object ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.isUninitialized() ) { + final EventSource source = event.getSession(); + final EntityPersister persister = source.getFactory().getMappingMetamodel() + .findEntityDescriptor( lazyInitializer.getEntityName() ); + final Object id = lazyInitializer.getIdentifier(); + final EntityKey key = source.generateEntityKey( id, persister ); + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); + if ( !persistenceContext.containsEntity( key ) + && canBeDeletedWithoutLoading( source, persister ) ) { + // optimization for deleting certain entities without loading them + persistenceContext.reassociateProxy( object, id ); + if ( !persistenceContext.containsDeletedUnloadedEntityKey( key ) ) { + persistenceContext.registerDeletedUnloadedEntityKey( key ); + + if ( persister.hasOwnedCollections() ) { + // we're deleting an unloaded proxy with collections + for ( Type type : persister.getPropertyTypes() ) { //TODO: when we enable this for subclasses use getSubclassPropertyTypeClosure() + deleteOwnedCollections( type, id, source ); + } + } - return fetchAndDelete( event, transientEntities, source, event.getObject() ); + ((ReactiveSession) source).getReactiveActionQueue() + .addAction( new ReactiveEntityDeleteAction( id, persister, source ) ); + } + return true; + } + } + } + return false; + } + + private static void deleteOwnedCollections(Type type, Object key, EventSource session) { + final MappingMetamodelImplementor mappingMetamodel = session.getFactory().getMappingMetamodel(); + final ReactiveActionQueue actionQueue = ((ReactiveSession) session).getReactiveActionQueue(); + if ( type.isCollectionType() ) { + final String role = ( (CollectionType) type ).getRole(); + final CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role); + if ( !persister.isInverse() ) { + actionQueue.addAction( new ReactiveCollectionRemoveAction( persister, key, session ) ); + } + } + else if ( type.isComponentType() ) { + final Type[] subtypes = ( (CompositeType) type ).getSubtypes(); + for ( Type subtype : subtypes ) { + deleteOwnedCollections( subtype, key, session ); + } + } } private CompletionStage fetchAndDelete( @@ -137,51 +210,50 @@ private CompletionStage fetchAndDelete( } private CompletionStage reactiveOnDelete(DeleteEvent event, DeleteContext transientEntities, Object entity) { + final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal(); + final EntityEntry entityEntry = persistenceContext.getEntry( entity ); + if ( entityEntry == null ) { + return deleteTransientInstance( event, transientEntities, entity ); + } + else { + return deletePersistentInstance( event, transientEntities, entity, entityEntry ); + } + } - EventSource source = event.getSession(); - PersistenceContext persistenceContext = source.getPersistenceContextInternal(); + private CompletionStage deleteTransientInstance(DeleteEvent event, DeleteContext transientEntities, Object entity) { + LOG.trace( "Entity was not persistent in delete processing" ); - EntityEntry entityEntry = persistenceContext.getEntry( entity ); + final EventSource source = event.getSession(); - if ( entityEntry == null ) { - LOG.trace( "Entity was not persistent in delete processing" ); - - final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - - return ForeignKeys.isTransient( persister.getEntityName(), entity, null, source.getSession() ) - .thenCompose( trans -> { - if ( trans ) { - // EARLY EXIT!!! - return deleteTransientEntity( - source, - entity, - event.isCascadeDeleteEnabled(), - persister, - transientEntities - ); - } - performDetachedEntityDeletionCheck(event); - - final Object id = persister.getIdentifier( entity, source); + final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity); + return ForeignKeys.isTransient( persister.getEntityName(), entity, null, source.getSession() ) + .thenCompose( trans -> { + if ( trans ) { + return deleteTransientEntity( source, entity, persister, transientEntities ); + } + else { + performDetachedEntityDeletionCheck( event ); + final Object id = persister.getIdentifier( entity, source ); if ( id == null ) { throw new TransientObjectException( "the detached instance passed to delete() had a null identifier" ); } + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); final EntityKey key = source.generateEntityKey( id, persister ); persistenceContext.checkUniqueness( key, entity ); - new OnUpdateVisitor(source, id, entity ).process( entity, persister ); + new OnUpdateVisitor( source, id, entity ).process( entity, persister ); final Object version = persister.getVersion( entity ); - EntityEntry entry = persistenceContext.addEntity( + final EntityEntry entry = persistenceContext.addEntity( entity, - ( persister.isMutable() ? Status.MANAGED : Status.READ_ONLY ), - persister.getPropertyValues( entity ), + persister.isMutable() ? Status.MANAGED : Status.READ_ONLY, + persister.getValues( entity ), key, version, LockMode.NONE, @@ -189,57 +261,97 @@ private CompletionStage reactiveOnDelete(DeleteEvent event, DeleteContext persister, false ); - persister.afterReassociate( entity, source); + persister.afterReassociate( entity, source ); - callbackRegistry.preRemove( entity ); + return delete( event, transientEntities, source, entity, persister, id, version, entry ); + } + } ); + } - return deleteEntity( - source, - entity, - entry, - event.isCascadeDeleteEnabled(), - event.isOrphanRemovalBeforeUpdates(), - persister, - transientEntities - ) - .thenAccept( v -> { - if ( source.getFactory().getSessionFactoryOptions().isIdentifierRollbackEnabled() ) { - persister.resetIdentifier( entity, id, version, source); - } - } ); - } ); + private CompletionStage deletePersistentInstance( + DeleteEvent event, + DeleteContext transientEntities, + Object entity, + EntityEntry entityEntry) { + LOG.trace( "Deleting a persistent instance" ); + final EventSource source = event.getSession(); + if ( entityEntry.getStatus().isDeletedOrGone() + || source.getPersistenceContextInternal() + .containsDeletedUnloadedEntityKey( entityEntry.getEntityKey() ) ) { + LOG.trace( "Object was already deleted" ); + return voidFuture(); } else { - LOG.trace( "Deleting a persistent instance" ); - - Status status = entityEntry.getStatus(); - if ( status == Status.DELETED || status == Status.GONE ) { - LOG.trace( "Object was already deleted" ); - return voidFuture(); - } - final EntityPersister persister = entityEntry.getPersister(); - Object id = entityEntry.getId(); - Object version = entityEntry.getVersion(); - - callbackRegistry.preRemove( entity ); - - return deleteEntity( + return delete( + event, + transientEntities, source, entity, - entityEntry, - event.isCascadeDeleteEnabled(), - event.isOrphanRemovalBeforeUpdates(), - persister, - transientEntities - ) - .thenAccept( v -> { - if ( source.getFactory().getSessionFactoryOptions().isIdentifierRollbackEnabled() ) { - persister.resetIdentifier( entity, id, version, source); - } - } ); + entityEntry.getPersister(), + entityEntry.getId(), + entityEntry.getVersion(), + entityEntry + ); } } + private CompletionStage delete( + DeleteEvent event, + DeleteContext transientEntities, + EventSource source, + Object entity, + EntityPersister persister, + Object id, + Object version, + EntityEntry entry) { + callbackRegistry.preRemove( entity ); + return deleteEntity( + source, + entity, + entry, + event.isCascadeDeleteEnabled(), + event.isOrphanRemovalBeforeUpdates(), + persister, + transientEntities + ) + .thenAccept( v -> { + if ( source.getFactory().getSessionFactoryOptions().isIdentifierRollbackEnabled() ) { + persister.resetIdentifier( entity, id, version, source ); + } + } ); + } + + /** + * Can we delete the row represented by the proxy without loading the entity? + */ + private boolean canBeDeletedWithoutLoading(EventSource source, EntityPersister persister) { + return source.getInterceptor() == EmptyInterceptor.INSTANCE + && !persister.implementsLifecycle() + && !persister.hasSubclasses() //TODO: should be unnecessary, using EntityPersister.getSubclassPropertyTypeClosure(), etc + && !persister.hasCascadeDelete() + && !persister.hasNaturalIdentifier() + && !persister.hasCollectionNotReferencingPK() + && !hasRegisteredRemoveCallbacks( persister ) + && !hasCustomEventListeners( source ); + } + + private static boolean hasCustomEventListeners(EventSource source) { + final FastSessionServices fss = source.getFactory().getFastSessionServices(); + // Bean Validation adds a PRE_DELETE listener + // and Envers adds a POST_DELETE listener + return fss.eventListenerGroup_PRE_DELETE.count() > 0 + || fss.eventListenerGroup_POST_DELETE.count() > 1 + || fss.eventListenerGroup_POST_DELETE.count() == 1 + && !(fss.eventListenerGroup_POST_DELETE.listeners().iterator().next() + instanceof PostDeleteEventListenerStandardImpl); + } + + private boolean hasRegisteredRemoveCallbacks(EntityPersister persister) { + final Class mappedClass = persister.getMappedClass(); + return callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.PRE_REMOVE ) + || callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.POST_REMOVE ); + } + /** * Called when we have recognized an attempt to delete a detached entity. *

@@ -275,7 +387,6 @@ private void disallowDeletionOfDetached(DeleteEvent event) { * * @param session The session which is the source of the event * @param entity The entity being delete processed - * @param cascadeDeleteEnabled Is cascading of deletes enabled * @param persister The entity persister * @param transientEntities A cache of already visited transient entities * (to avoid infinite recursion). @@ -283,17 +394,17 @@ private void disallowDeletionOfDetached(DeleteEvent event) { protected CompletionStage deleteTransientEntity( EventSource session, Object entity, - boolean cascadeDeleteEnabled, EntityPersister persister, DeleteContext transientEntities) { LOG.handlingTransientEntity(); - if ( !transientEntities.add( entity ) ) { + if ( transientEntities.add( entity ) ) { + return cascadeBeforeDelete( session, persister, entity, transientEntities ) + .thenCompose( v -> cascadeAfterDelete( session, persister, entity, transientEntities ) ); + } + else { LOG.trace( "Already handled transient entity; skipping" ); return voidFuture(); } - transientEntities.add( entity ); - return cascadeBeforeDelete( session, persister, entity, null, transientEntities ) - .thenCompose( v -> cascadeAfterDelete( session, persister, entity, transientEntities ) ); } /** @@ -322,24 +433,28 @@ protected CompletionStage deleteEntity( } final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - final Type[] propTypes = persister.getPropertyTypes(); final Object version = entityEntry.getVersion(); final Object[] currentState = entityEntry.getLoadedState() == null ? persister.getValues( entity ) // i.e. the entity came in from update() : entityEntry.getLoadedState(); - final Object[] deletedState = createDeletedState( persister, currentState, session ); + final Object[] deletedState = createDeletedState( persister, entity, currentState, session ); entityEntry.setDeletedState( deletedState ); - session.getInterceptor() - .onDelete( entity, entityEntry.getId(), deletedState, persister.getPropertyNames(), propTypes ); + session.getInterceptor().onDelete( + entity, + entityEntry.getId(), + deletedState, + persister.getPropertyNames(), + persister.getPropertyTypes() + ); - // before any callbacks, etc, so subdeletions see that this deletion happened first + // before any callbacks, etc., so subdeletions see that this deletion happened first persistenceContext.setEntryStatus( entityEntry, Status.DELETED ); final EntityKey key = session.generateEntityKey( entityEntry.getId(), persister ); - return cascadeBeforeDelete( session, persister, entity, entityEntry, transientEntities ) + return cascadeBeforeDelete( session, persister, entity, transientEntities ) .thenCompose( v -> new ForeignKeys.Nullifier( entity, true, @@ -355,7 +470,7 @@ protected CompletionStage deleteEntity( ); persistenceContext.registerNullifiableEntityKey( key ); - ReactiveActionQueue actionQueue = actionQueue( session ); + final ReactiveActionQueue actionQueue = actionQueue( session ); if ( isOrphanRemovalBeforeUpdates ) { // TODO: The removeOrphan concept is a temporary "hack" for HHH-6484. This should be removed once action/task @@ -394,12 +509,44 @@ private ReactiveActionQueue actionQueue(EventSource session) { return session.unwrap( ReactiveSession.class ).getReactiveActionQueue(); } - private Object[] createDeletedState(EntityPersister persister, Object[] currentState, EventSource session) { - Type[] propTypes = persister.getPropertyTypes(); - final Object[] deletedState = new Object[propTypes.length]; - boolean[] copyability = new boolean[propTypes.length]; - java.util.Arrays.fill( copyability, true ); - TypeHelper.deepCopy( currentState, propTypes, copyability, deletedState, session ); + private Object[] createDeletedState( + EntityPersister persister, + Object parent, + Object[] currentState, + EventSource eventSource) { + final Type[] types = persister.getPropertyTypes(); + final Object[] deletedState = new Object[types.length]; + if ( !persister.hasCollections() || !persister.hasUninitializedLazyProperties( parent ) ) { + boolean[] copyability = new boolean[types.length]; + java.util.Arrays.fill( copyability, true ); + TypeHelper.deepCopy( currentState, types, copyability, deletedState, eventSource ); + return deletedState; + } + + final String[] propertyNames = persister.getPropertyNames(); + final BytecodeEnhancementMetadata enhancementMetadata = persister.getBytecodeEnhancementMetadata(); + for ( int i = 0; i < types.length; i++) { + if ( types[i].isCollectionType() && !enhancementMetadata.isAttributeLoaded( parent, propertyNames[i] ) ) { + final CollectionType collectionType = (CollectionType) types[i]; + final CollectionPersister collectionDescriptor = persister.getFactory().getMappingMetamodel() + .getCollectionDescriptor( collectionType.getRole() ); + if ( collectionDescriptor.needsRemove() || collectionDescriptor.hasCache() ) { + final Object keyOfOwner = collectionType.getKeyOfOwner( parent, eventSource.getSession() ); + // This will make sure that a CollectionEntry exists + deletedState[i] = collectionType.getCollection( keyOfOwner, eventSource.getSession(), parent, false ); + } + else { + deletedState[i] = currentState[i]; + } + } + else if ( currentState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY + || currentState[i] == PropertyAccessStrategyBackRefImpl.UNKNOWN ) { + deletedState[i] = currentState[i]; + } + else { + deletedState[i] = types[i].deepCopy( currentState[i], eventSource.getFactory() ); + } + } return deletedState; } @@ -407,12 +554,11 @@ protected CompletionStage cascadeBeforeDelete( EventSource session, EntityPersister persister, Object entity, - EntityEntry entityEntry, DeleteContext transientEntities) throws HibernateException { // cascade-delete to collections BEFORE the collection owner is deleted return fetchLazyAssociationsBeforeCascade( CascadingActions.DELETE, persister, entity, session ) .thenCompose( - v -> new Cascade( + v -> new Cascade<>( CascadingActions.DELETE, CascadePoint.AFTER_INSERT_BEFORE_DELETE, persister, entity, transientEntities, session diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java index 9f9bfd85e..0956e8e6a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java @@ -5,26 +5,16 @@ */ package org.hibernate.reactive.event.impl; -import static org.hibernate.pretty.MessageHelper.infoString; - -import java.lang.invoke.MethodHandles; -import java.util.Arrays; - import org.hibernate.AssertionFailure; import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.HibernateException; -import org.hibernate.SessionFactory; import org.hibernate.StaleObjectStateException; import org.hibernate.action.internal.DelayedPostInsertIdentifier; -import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; -import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.engine.internal.Nullability; import org.hibernate.engine.internal.Versioning; import org.hibernate.engine.spi.EntityEntry; -import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; -import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -41,6 +31,7 @@ import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.pretty.MessageHelper; import org.hibernate.reactive.engine.impl.ReactiveEntityUpdateAction; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; @@ -48,6 +39,20 @@ import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.Type; +import java.lang.invoke.MethodHandles; +import java.util.Arrays; + +import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTracker; +import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker; +import static org.hibernate.engine.internal.Versioning.getVersion; +import static org.hibernate.engine.internal.Versioning.incrementVersion; +import static org.hibernate.engine.internal.Versioning.setVersion; +import static org.hibernate.pretty.MessageHelper.infoString; + /** * A reactific {@link org.hibernate.event.internal.DefaultFlushEntityEventListener}. * This implementation is almost, but not quite, a line-for-line copy of @@ -74,20 +79,28 @@ public void injectCallbackRegistry(CallbackRegistry callbackRegistry) { public void checkId(Object object, EntityPersister persister, Object id, SessionImplementor session) throws HibernateException { - if (id instanceof DelayedPostInsertIdentifier) { + if ( id instanceof DelayedPostInsertIdentifier ) { // this is a situation where the entity id is assigned by a post-insert generator // and was saved outside the transaction forcing it to be delayed return; } - if ( persister.canExtractIdOutOfEntity() ) { - Object oid = persister.getIdentifier( object, session ); - if ( id == null ) { - throw new AssertionFailure( "null id in " + persister.getEntityName() + " entry (don't flush the Session after an exception occurs)" ); - } - if ( !persister.getIdentifierType().isEqual( id, oid, session.getFactory() ) ) { - throw LOG.identifierAltered( persister.getEntityName(), id, oid ); - } + final Object oid = persister.getIdentifier( object, session ); + + if ( id == null ) { + throw new AssertionFailure( "null id in " + persister.getEntityName() + + " entry (don't flush the Session after an exception occurs)" ); + } + + //Small optimisation: always try to avoid getIdentifierType().isEqual(..) when possible. + //(However it's not safe to invoke the equals() method as it might trigger side-effects) + if ( id == oid ) { + //No further checks necessary: + return; + } + + if ( !persister.getIdentifierType().isEqual( id, oid, session.getFactory() ) ) { + throw LOG.identifierAltered( persister.getEntityName(), id, oid ); } } @@ -98,20 +111,22 @@ private void checkNaturalId( Object[] current, Object[] loaded, SessionImplementor session) { - - if ( entity instanceof PersistentAttributeInterceptable ) { - final PersistentAttributeInterceptor interceptor = - ( (PersistentAttributeInterceptable) entity ).$$_hibernate_getInterceptor(); - if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) { - // EARLY EXIT!!! - // nothing to check - the entity is an un-initialized enhancement-as-proxy reference - return; + if ( !isUninitializedEnhanced( entity ) ) { + final NaturalIdMapping naturalIdMapping = persister.getNaturalIdMapping(); + if ( naturalIdMapping != null && entry.getStatus() != Status.READ_ONLY ) { + naturalIdMapping.verifyFlushState( entry.getId(), current, loaded, session ); } } + } - final NaturalIdMapping naturalIdMapping = persister.getNaturalIdMapping(); - if ( naturalIdMapping != null && entry.getStatus() != Status.READ_ONLY ) { - naturalIdMapping.verifyFlushState( entry.getId(), current, loaded, session ); + private static boolean isUninitializedEnhanced(Object entity) { + if ( isPersistentAttributeInterceptable( entity ) ) { + final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + // the entity is an un-initialized enhancement-as-proxy reference + return interceptor instanceof EnhancementAsProxyLazinessInterceptor; + } + else { + return false; } } @@ -123,9 +138,6 @@ public void onFlushEntity(FlushEntityEvent event) throws HibernateException { final Object entity = event.getEntity(); final EntityEntry entry = event.getEntityEntry(); final EventSource session = event.getSession(); - final EntityPersister persister = entry.getPersister(); - final Status status = entry.getStatus(); - final Type[] types = persister.getPropertyTypes(); final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); @@ -134,14 +146,17 @@ public void onFlushEntity(FlushEntityEvent event) throws HibernateException { event.setPropertyValues( values ); //TODO: avoid this for non-new instances where mightBeDirty==false - boolean substitute = wrapCollections( session, persister, entity, entry.getId(), types, values ); + + boolean substitute = wrapCollections( event, values ); if ( isUpdateNecessary( event, mightBeDirty ) ) { substitute = scheduleUpdate( event ) || substitute; } - if ( status != Status.DELETED ) { - // now update the object .. has to be outside the main if block above (because of collections) + if ( entry.getStatus() != Status.DELETED ) { + final EntityPersister persister = entry.getPersister(); + // now update the object + // has to be outside the main if block above (because of collections) if ( substitute ) { persister.setPropertyValues( entity, values ); } @@ -149,59 +164,47 @@ public void onFlushEntity(FlushEntityEvent event) throws HibernateException { // Search for collections by reachability, updating their role. // We don't want to touch collections reachable from a deleted object if ( persister.hasCollections() ) { - new FlushVisitor( session, entity ).processEntityPropertyValues( values, types ); + new FlushVisitor( session, entity ) + .processEntityPropertyValues( values, persister.getPropertyTypes() ); } } } private Object[] getValues(Object entity, EntityEntry entry, boolean mightBeDirty, SessionImplementor session) { final Object[] loadedState = entry.getLoadedState(); - final Status status = entry.getStatus(); - final EntityPersister persister = entry.getPersister(); - final Object[] values; - if ( status == Status.DELETED ) { + if ( entry.getStatus() == Status.DELETED ) { //grab its state saved at deletion - values = entry.getDeletedState(); + return entry.getDeletedState(); } else if ( !mightBeDirty && loadedState != null ) { - values = loadedState; + return loadedState; } else { + final EntityPersister persister = entry.getPersister(); checkId( entity, persister, entry.getId(), session ); - // grab its current state - values = persister.getValues( entity ); - + Object[] values = persister.getValues( entity ); checkNaturalId( persister, entity, entry, values, loadedState, session ); + return values; } - return values; } - /* - * see DefaultFlushEntityEventListener#wrapCollections + /** + * Wrap up any new collections directly referenced by the object + * or its components. */ - private boolean wrapCollections( - EventSource session, - EntityPersister persister, - Object entity, - Object id, - Type[] types, - Object[] values - ) { + private boolean wrapCollections(FlushEntityEvent event, Object[] values) { + final EntityEntry entry = event.getEntityEntry(); + final EntityPersister persister = entry.getPersister(); if ( persister.hasCollections() ) { - - // wrap up any new collections directly referenced by the object - // or its components - - // NOTE: we need to do the wrap here even if its not "dirty", + // NOTE: we need to do the wrap here even if it's not "dirty", // because collections need wrapping but changes to _them_ // don't dirty the container. Also, for versioned data, we // need to wrap before calling searchForDirtyCollections - - WrapVisitor visitor = new WrapVisitor( entity, id ,session ); - // substitutes into values by side-effect - visitor.processEntityPropertyValues( values, types ); + final WrapVisitor visitor = new WrapVisitor( event.getEntity(), entry.getId(), event.getSession() ); + // substitutes into values by side effect + visitor.processEntityPropertyValues( values, persister.getPropertyTypes() ); return visitor.isSubstitutionRequired(); } else { @@ -210,7 +213,8 @@ private boolean wrapCollections( } private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) { - final Status status = event.getEntityEntry().getStatus(); + final EntityEntry entityEntry = event.getEntityEntry(); + final Status status = entityEntry.getStatus(); if ( mightBeDirty || status == Status.DELETED ) { // compare to cached state (ignoring collections unless versioned) dirtyCheck( event ); @@ -219,16 +223,16 @@ private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mi } else { final Object entity = event.getEntity(); - ManagedTypeHelper.processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes ); - event.getSession() - .getFactory() + processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes ); + final EventSource source = event.getSession(); + source.getFactory() .getCustomEntityDirtinessStrategy() - .resetDirty( entity, event.getEntityEntry().getPersister(), event.getSession() ); + .resetDirty( entity, entityEntry.getPersister(), source ); return false; } } else { - return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status ); + return hasDirtyCollections( event ); } } @@ -240,48 +244,14 @@ private boolean scheduleUpdate(final FlushEntityEvent event) { final EntityPersister persister = entry.getPersister(); final Object[] values = event.getPropertyValues(); - if ( LOG.isTraceEnabled() ) { - if ( status == Status.DELETED ) { - if ( !persister.isMutable() ) { - LOG.tracev( - "Updating immutable, deleted entity: {0}", - infoString( persister, entry.getId(), session.getFactory() ) - ); - } - else if ( !entry.isModifiableEntity() ) { - LOG.tracev( - "Updating non-modifiable, deleted entity: {0}", - infoString( persister, entry.getId(), session.getFactory() ) - ); - } - else { - LOG.tracev( - "Updating deleted entity: ", - infoString( persister, entry.getId(), session.getFactory() ) - ); - } - } - else { - LOG.tracev( - "Updating entity: {0}", - infoString( persister, entry.getId(), session.getFactory() ) - ); - } - } + logScheduleUpdate( entry, session, status, persister ); final boolean intercepted = !entry.isBeingReplicated() && handleInterception( event ); // increment the version number (if necessary) final Object nextVersion = getNextVersion( event ); - // if it was dirtied by a collection only - int[] dirtyProperties = event.getDirtyProperties(); - if ( event.isDirtyCheckPossible() && dirtyProperties == null ) { - if ( !intercepted && !event.hasDirtyCollection() ) { - throw new AssertionFailure( "dirty, but no dirty properties" ); - } - dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY; - } + int[] dirtyProperties = getDirtyProperties( event, intercepted ); // check nullability but do not doAfterTransactionCompletion command execute // we'll use scheduled updates for that. @@ -295,9 +265,9 @@ else if ( !entry.isModifiableEntity() ) { values, dirtyProperties, event.hasDirtyCollection(), - ( status == Status.DELETED && !entry.isModifiableEntity() ? - persister.getPropertyValues( entity ) : - entry.getLoadedState() ), + status == Status.DELETED && !entry.isModifiableEntity() + ? persister.getPropertyValues( entity ) + : entry.getLoadedState(), entry.getVersion(), nextVersion, entity, @@ -310,63 +280,97 @@ else if ( !entry.isModifiableEntity() ) { return intercepted; } - protected boolean handleInterception(FlushEntityEvent event) { - SessionImplementor session = event.getSession(); - EntityEntry entry = event.getEntityEntry(); - EntityPersister persister = entry.getPersister(); - Object entity = event.getEntity(); + private static int[] getDirtyProperties(FlushEntityEvent event, boolean intercepted) { + int[] dirtyProperties = event.getDirtyProperties(); + if ( event.isDirtyCheckPossible() && dirtyProperties == null ) { + if ( !intercepted && !event.hasDirtyCollection() ) { + throw new AssertionFailure( "dirty, but no dirty properties" ); + } + else { + // it was dirtied by a collection only + return ArrayHelper.EMPTY_INT_ARRAY; + } + } + else { + return dirtyProperties; + } + } - //give the Interceptor a chance to modify property values - final Object[] values = event.getPropertyValues(); - final boolean intercepted = invokeInterceptor( session, entity, entry, values, persister ); + private static void logScheduleUpdate(EntityEntry entry, EventSource session, Status status, EntityPersister persister) { + if ( LOG.isTraceEnabled() ) { + if ( status == Status.DELETED ) { + if ( !persister.isMutable() ) { + LOG.tracev( + "Updating immutable, deleted entity: {0}", + MessageHelper.infoString(persister, entry.getId(), session.getFactory() ) + ); + } + else if ( !entry.isModifiableEntity() ) { + LOG.tracev( + "Updating non-modifiable, deleted entity: {0}", + MessageHelper.infoString(persister, entry.getId(), session.getFactory() ) + ); + } + else { + LOG.tracev( + "Updating deleted entity: {0}", + MessageHelper.infoString(persister, entry.getId(), session.getFactory() ) + ); + } + } + else { + LOG.tracev( + "Updating entity: {0}", + MessageHelper.infoString(persister, entry.getId(), session.getFactory() ) + ); + } + } + } + protected boolean handleInterception(FlushEntityEvent event) { + //give the Interceptor a chance to modify property values + final boolean intercepted = invokeInterceptor( event ); //now we might need to recalculate the dirtyProperties array if ( intercepted && event.isDirtyCheckPossible() ) { dirtyCheck( event ); } - return intercepted; } - protected boolean invokeInterceptor( - SessionImplementor session, - Object entity, - EntityEntry entry, - final Object[] values, - EntityPersister persister) { + protected boolean invokeInterceptor(FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + final Object id = entry.getId(); + final Object[] values = event.getPropertyValues(); + final EntityPersister persister = entry.getPersister(); + final EventSource session = event.getSession(); + boolean isDirty = false; + if ( entry.getStatus() != Status.DELETED ) { if ( callbackRegistry.preUpdate( entity ) ) { isDirty = copyState( entity, persister.getPropertyTypes(), values, session.getFactory() ); } } - final boolean answerFromInterceptor = session.getInterceptor().onFlushDirty( + final boolean stateModified = session.getInterceptor().onFlushDirty( entity, - entry.getId(), + id, values, entry.getLoadedState(), persister.getPropertyNames(), persister.getPropertyTypes() ); - return answerFromInterceptor || isDirty; + return stateModified || isDirty; } - private boolean copyState(Object entity, Type[] types, Object[] state, SessionFactory sf) { + private boolean copyState(Object entity, Type[] types, Object[] state, SessionFactoryImplementor factory) { // copy the entity state into the state array and return true if the state has changed - final Object[] newState = sf.unwrap( SessionFactoryImplementor.class ) - .getRuntimeMetamodels() - .getEntityMappingType( entity.getClass() ) - .getEntityPersister() - .getValues( entity ); - - int size = newState.length; + final Object[] newState = currentState( entity, factory ); boolean isDirty = false; - for ( int index = 0; index < size; index++ ) { - if ( ( state[index] == LazyPropertyInitializer.UNFETCHED_PROPERTY && - newState[index] != LazyPropertyInitializer.UNFETCHED_PROPERTY ) || - ( state[index] != newState[index] && !types[index].isEqual( state[index], newState[index] ) ) ) { + for ( int index = 0, size = newState.length; index < size; index++ ) { + if ( isDirty( types[index], state[index], newState[index] ) ) { isDirty = true; state[index] = newState[index]; } @@ -374,58 +378,55 @@ private boolean copyState(Object entity, Type[] types, Object[] state, SessionFa return isDirty; } + private static Object[] currentState(Object entity, SessionFactoryImplementor factory) { + return factory.getRuntimeMetamodels() + .getEntityMappingType( entity.getClass() ) + .getEntityPersister() + .getValues( entity ); + } + + private static boolean isDirty(Type types, Object state, Object newState) { + return state == UNFETCHED_PROPERTY && newState != UNFETCHED_PROPERTY + || state != newState && !types.isEqual( state, newState ); + } + /** * Convenience method to retrieve an entities next version value */ private Object getNextVersion(FlushEntityEvent event) throws HibernateException { - - EntityEntry entry = event.getEntityEntry(); - EntityPersister persister = entry.getPersister(); + final EntityEntry entry = event.getEntityEntry(); + final EntityPersister persister = entry.getPersister(); if ( persister.isVersioned() ) { - - Object[] values = event.getPropertyValues(); - + final Object[] values = event.getPropertyValues(); if ( entry.isBeingReplicated() ) { - return Versioning.getVersion( values, persister ); + return getVersion( values, persister ); } else { - int[] dirtyProperties = event.getDirtyProperties(); - - final boolean isVersionIncrementRequired = isVersionIncrementRequired( - event, - entry, - persister, - dirtyProperties - ); - - final Object nextVersion = isVersionIncrementRequired - ? Versioning.increment( entry.getVersion(), persister.getVersionMapping(), event.getSession() ) + final Object nextVersion = isVersionIncrementRequired( event, entry ) + ? incrementVersion( event.getEntity(), entry.getVersion(), persister, event.getSession() ) : entry.getVersion(); //use the current version - - Versioning.setVersion( values, nextVersion, persister ); - + setVersion( values, nextVersion, persister ); return nextVersion; } } else { return null; } - } - private boolean isVersionIncrementRequired( - FlushEntityEvent event, - EntityEntry entry, - EntityPersister persister, - int[] dirtyProperties) { - return entry.getStatus() != Status.DELETED && ( - dirtyProperties == null || - Versioning.isVersionIncrementRequired( - dirtyProperties, - event.hasDirtyCollection(), - persister.getPropertyVersionability() - ) - ); + private boolean isVersionIncrementRequired(FlushEntityEvent event, EntityEntry entry) { + if ( entry.getStatus() == Status.DELETED ) { + return false; + } + else { + int[] dirtyProperties = event.getDirtyProperties(); + return dirtyProperties == null + || Versioning.isVersionIncrementRequired( + dirtyProperties, + event.hasDirtyCollection(), + event.getEntityEntry().getPersister().getPropertyVersionability() + ); + } } /** @@ -434,29 +435,17 @@ private boolean isVersionIncrementRequired( * Note: this method is quite slow, avoid calling if possible! */ protected final boolean isUpdateNecessary(FlushEntityEvent event) throws HibernateException { + return !event.isDirtyCheckPossible() + || event.hasDirtyProperties() + || hasDirtyCollections(event); - EntityPersister persister = event.getEntityEntry().getPersister(); - Status status = event.getEntityEntry().getStatus(); - - if ( !event.isDirtyCheckPossible() ) { - return true; - } - else { - - int[] dirtyProperties = event.getDirtyProperties(); - if ( dirtyProperties != null && dirtyProperties.length != 0 ) { - return true; //TODO: suck into event class - } - else { - return hasDirtyCollections( event, persister, status ); - } - - } } - private boolean hasDirtyCollections(FlushEntityEvent event, EntityPersister persister, Status status) { - if ( isCollectionDirtyCheckNecessary( persister, status ) ) { - DirtyCollectionSearchVisitor visitor = new DirtyCollectionSearchVisitor( + private boolean hasDirtyCollections(FlushEntityEvent event) { + final EntityEntry entityEntry = event.getEntityEntry(); + final EntityPersister persister = entityEntry.getPersister(); + if ( isCollectionDirtyCheckNecessary( persister, entityEntry.getStatus() ) ) { + final DirtyCollectionSearchVisitor visitor = new DirtyCollectionSearchVisitor( event.getEntity(), event.getSession(), persister.getPropertyVersionability() @@ -472,136 +461,156 @@ private boolean hasDirtyCollections(FlushEntityEvent event, EntityPersister pers } private boolean isCollectionDirtyCheckNecessary(EntityPersister persister, Status status) { - return ( status == Status.MANAGED || status == Status.READ_ONLY ) && - persister.isVersioned() && - persister.hasCollections(); + return ( status == Status.MANAGED || status == Status.READ_ONLY ) + && persister.isVersioned() + && persister.hasCollections(); } /** * Perform a dirty check, and attach the results to the event */ protected void dirtyCheck(final FlushEntityEvent event) throws HibernateException { + int[] dirtyProperties = getDirtyProperties( event ); + event.setDatabaseSnapshot( null ); + if ( dirtyProperties == null ) { + // do the dirty check the hard way + dirtyProperties = performDirtyCheck( event ); + } + else { + // the Interceptor, SelfDirtinessTracker, or CustomEntityDirtinessStrategy + // already handled the dirty check for us + event.setDirtyProperties( dirtyProperties ); + event.setDirtyCheckHandledByInterceptor( true ); + event.setDirtyCheckPossible( true ); + } + logDirtyProperties( event.getEntityEntry(), dirtyProperties ); + } - final Object entity = event.getEntity(); - final Object[] values = event.getPropertyValues(); + private static int[] performDirtyCheck(FlushEntityEvent event) { final SessionImplementor session = event.getSession(); - final EntityEntry entry = event.getEntityEntry(); - final EntityPersister persister = entry.getPersister(); - final Object id = entry.getId(); - final Object[] loadedState = entry.getLoadedState(); - - int[] dirtyProperties = session.getInterceptor().findDirty( - entity, - id, - values, - loadedState, - persister.getPropertyNames(), - persister.getPropertyTypes() - ); - - if ( dirtyProperties == null ) { - if ( ManagedTypeHelper.isSelfDirtinessTracker( entity ) ) { - final SelfDirtinessTracker tracker = ManagedTypeHelper.asSelfDirtinessTracker( entity ); - if ( tracker.$$_hibernate_hasDirtyAttributes() || persister.hasMutableProperties() ) { - dirtyProperties = persister.resolveDirtyAttributeIndexes( - values, - loadedState, - tracker.$$_hibernate_getDirtyAttributes(), - session + boolean dirtyCheckPossible; + int[] dirtyProperties = null; + try { + session.getEventListenerManager().dirtyCalculationStart(); + // object loaded by update() + final EntityEntry entry = event.getEntityEntry(); + final EntityPersister persister = entry.getPersister(); + final Object[] values = event.getPropertyValues(); + final Object[] loadedState = entry.getLoadedState(); + final Object entity = event.getEntity(); + if ( loadedState != null ) { + // dirty check against the usual snapshot of the entity + dirtyProperties = persister.findDirty( values, loadedState, entity, session ); + dirtyCheckPossible = true; + } + else if ( entry.getStatus() == Status.DELETED && !entry.isModifiableEntity() ) { + // A non-modifiable (e.g., read-only or immutable) entity needs to be have + // references to transient entities set to null before being deleted. No other + // fields should be updated. + if ( values != entry.getDeletedState() ) { + throw new IllegalStateException( + "Entity has status Status.DELETED but values != entry.getDeletedState" ); } - else { - dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY; - } + // Even if loadedState == null, we can dirty-check by comparing currentState and + // entry.getDeletedState() because the only fields to be updated are those that + // refer to transient entities that are being set to null. + // - currentState contains the entity's current property values. + // - entry.getDeletedState() contains the entity's current property values with + // references to transient entities set to null. + // - dirtyProperties will only contain properties that refer to transient entities + final Object[] currentState = persister.getValues( event.getEntity() ); + dirtyProperties = persister.findDirty( entry.getDeletedState(), currentState, entity, session ); + dirtyCheckPossible = true; } else { - // see if the custom dirtiness strategy can tell us... - class DirtyCheckContextImpl implements CustomEntityDirtinessStrategy.DirtyCheckContext { - private int[] found; - - @Override - public void doDirtyChecking(CustomEntityDirtinessStrategy.AttributeChecker attributeChecker) { - found = new DirtyCheckAttributeInfoImpl( event ).visitAttributes( attributeChecker ); - if ( found.length == 0 ) { - found = null; - } - } - } - DirtyCheckContextImpl context = new DirtyCheckContextImpl(); - session.getFactory().getCustomEntityDirtinessStrategy().findDirty( - entity, - persister, - session, - context - ); - dirtyProperties = context.found; - } - } - - event.setDatabaseSnapshot( null ); - - final boolean interceptorHandledDirtyCheck; - //The dirty check is considered possible unless proven otherwise (see below) - boolean dirtyCheckPossible = true; - - if ( dirtyProperties == null ) { - // Interceptor returned null, so do the dirtycheck ourself, if possible - try { - session.getEventListenerManager().dirtyCalculationStart(); - - interceptorHandledDirtyCheck = false; - // object loaded by update() - dirtyCheckPossible = loadedState != null; - if ( dirtyCheckPossible ) { - // dirty check against the usual snapshot of the entity - dirtyProperties = persister.findDirty( values, loadedState, entity, session ); - } - else if ( entry.getStatus() == Status.DELETED && !event.getEntityEntry().isModifiableEntity() ) { - // A non-modifiable (e.g., read-only or immutable) entity needs to be have - // references to transient entities set to null before being deleted. No other - // fields should be updated. - if ( values != entry.getDeletedState() ) { - throw LOG.entityDeleteStateIllegal(); - } - // Even if loadedState == null, we can dirty-check by comparing currentState and - // entry.getDeletedState() because the only fields to be updated are those that - // refer to transient entities that are being set to null. - // - currentState contains the entity's current property values. - // - entry.getDeletedState() contains the entity's current property values with - // references to transient entities set to null. - // - dirtyProperties will only contain properties that refer to transient entities - final Object[] currentState = persister.getPropertyValues( event.getEntity() ); - dirtyProperties = persister.findDirty( entry.getDeletedState(), currentState, entity, session ); + // dirty check against the database snapshot, if possible/necessary + final Object[] databaseSnapshot = getDatabaseSnapshot( persister, entry.getId(), session ); + if ( databaseSnapshot != null ) { + dirtyProperties = persister.findModified( databaseSnapshot, values, entity, session ); dirtyCheckPossible = true; + event.setDatabaseSnapshot( databaseSnapshot ); } else { - // dirty check against the database snapshot, if possible/necessary - final Object[] databaseSnapshot = getDatabaseSnapshot( session, persister, id ); - if ( databaseSnapshot != null ) { - dirtyProperties = persister.findModified( databaseSnapshot, values, entity, session ); - dirtyCheckPossible = true; - event.setDatabaseSnapshot( databaseSnapshot ); - } + dirtyCheckPossible = false; } } - finally { - session.getEventListenerManager().dirtyCalculationEnd( dirtyProperties != null ); - } + event.setDirtyProperties( dirtyProperties ); + event.setDirtyCheckHandledByInterceptor( false ); + event.setDirtyCheckPossible( dirtyCheckPossible ); + } + finally { + session.getEventListenerManager().dirtyCalculationEnd( dirtyProperties != null ); + } + return dirtyProperties; + } + + /** + * Attempt to get the dirty properties from either the Interceptor, + * the bytecode enhancement, or a custom dirtiness strategy. + */ + private static int[] getDirtyProperties(FlushEntityEvent event) { + int[] dirtyProperties = getDirtyPropertiesFromInterceptor( event ); + if ( dirtyProperties != null ) { + return dirtyProperties; } else { - // either the Interceptor, the bytecode enhancement or a custom dirtiness strategy handled the dirty checking - interceptorHandledDirtyCheck = true; + final Object entity = event.getEntity(); + return isSelfDirtinessTracker( entity ) + ? getDirtyPropertiesFromSelfDirtinessTracker( asSelfDirtinessTracker( entity ), event ) + : getDirtyPropertiesFromCustomEntityDirtinessStrategy( event ); } + } - logDirtyProperties( id, dirtyProperties, persister ); + private static int[] getDirtyPropertiesFromInterceptor(FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final EntityPersister persister = entry.getPersister(); + return event.getSession().getInterceptor().findDirty( + event.getEntity(), + entry.getId(), + event.getPropertyValues(), + entry.getLoadedState(), + persister.getPropertyNames(), + persister.getPropertyTypes() + ); + } - event.setDirtyProperties( dirtyProperties ); - event.setDirtyCheckHandledByInterceptor( interceptorHandledDirtyCheck ); - event.setDirtyCheckPossible( dirtyCheckPossible ); + private static int[] getDirtyPropertiesFromCustomEntityDirtinessStrategy(FlushEntityEvent event) { + // see if the custom dirtiness strategy can tell us... + class DirtyCheckContextImpl implements CustomEntityDirtinessStrategy.DirtyCheckContext { + private int[] found; + @Override + public void doDirtyChecking(CustomEntityDirtinessStrategy.AttributeChecker attributeChecker) { + found = new DirtyCheckAttributeInfoImpl( event ).visitAttributes( attributeChecker ); + if ( found.length == 0 ) { + found = null; + } + } + } + final EventSource session = event.getSession(); + final DirtyCheckContextImpl context = new DirtyCheckContextImpl(); + session.getFactory().getCustomEntityDirtinessStrategy() + .findDirty( event.getEntity(), event.getEntityEntry().getPersister(), session, context ); + return context.found; + } + private static int[] getDirtyPropertiesFromSelfDirtinessTracker(SelfDirtinessTracker tracker, FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final EntityPersister persister = entry.getPersister(); + if ( tracker.$$_hibernate_hasDirtyAttributes() || persister.hasMutableProperties() ) { + return persister.resolveDirtyAttributeIndexes( + event.getPropertyValues(), + entry.getLoadedState(), + tracker.$$_hibernate_getDirtyAttributes(), + event.getSession() + ); + } + else { + return ArrayHelper.EMPTY_INT_ARRAY; + } } - private class DirtyCheckAttributeInfoImpl implements CustomEntityDirtinessStrategy.AttributeInformation { + private static class DirtyCheckAttributeInfoImpl implements CustomEntityDirtinessStrategy.AttributeInformation { private final FlushEntityEvent event; private final EntityPersister persister; private final int numberOfAttributes; @@ -643,7 +652,7 @@ public Object getCurrentValue() { @Override public Object getLoadedValue() { if ( databaseSnapshot == null ) { - databaseSnapshot = getDatabaseSnapshot( event.getSession(), persister, event.getEntityEntry().getId() ); + databaseSnapshot = getDatabaseSnapshot( persister, event.getEntityEntry().getId(), event.getSession() ); } return databaseSnapshot[index]; } @@ -663,8 +672,9 @@ public int[] visitAttributes(CustomEntityDirtinessStrategy.AttributeChecker attr } } - private void logDirtyProperties(Object id, int[] dirtyProperties, EntityPersister persister) { + private void logDirtyProperties(EntityEntry entry, int[] dirtyProperties) { if ( dirtyProperties != null && dirtyProperties.length > 0 && LOG.isTraceEnabled() ) { + final EntityPersister persister = entry.getPersister(); final String[] allPropertyNames = persister.getPropertyNames(); final String[] dirtyPropertyNames = new String[dirtyProperties.length]; for ( int i = 0; i < dirtyProperties.length; i++ ) { @@ -672,30 +682,28 @@ private void logDirtyProperties(Object id, int[] dirtyProperties, EntityPersiste } LOG.tracev( "Found dirty properties [{0}] : {1}", - infoString( persister.getEntityName(), id ), + infoString( persister.getEntityName(), entry.getId() ), Arrays.toString( dirtyPropertyNames ) ); } } - private Object[] getDatabaseSnapshot(SessionImplementor session, EntityPersister persister, Object id) { + private static Object[] getDatabaseSnapshot(EntityPersister persister, Object id, SessionImplementor session) { final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); if ( persister.isSelectBeforeUpdateRequired() ) { - Object[] snapshot = persistenceContext.getDatabaseSnapshot( id, persister ); + final Object[] snapshot = persistenceContext.getDatabaseSnapshot( id, persister ); if ( snapshot == null ) { //do we even really need this? the update will fail anyway.... final StatisticsImplementor statistics = session.getFactory().getStatistics(); if ( statistics.isStatisticsEnabled() ) { - statistics - .optimisticFailure( persister.getEntityName() ); + statistics.optimisticFailure( persister.getEntityName() ); } throw new StaleObjectStateException( persister.getEntityName(), id ); } return snapshot; } // TODO: optimize away this lookup for entities w/o unsaved-value="undefined" - final EntityKey entityKey = session.generateEntityKey( id, persister ); - return persistenceContext.getCachedDatabaseSnapshot( entityKey ); + return persistenceContext.getCachedDatabaseSnapshot( session.generateEntityKey( id, persister ) ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEventListener.java index 9b3f67154..d850ce3e2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEventListener.java @@ -5,7 +5,6 @@ */ package org.hibernate.reactive.event.impl; -import java.lang.invoke.MethodHandles; import java.util.concurrent.CompletionStage; import org.hibernate.HibernateException; @@ -14,8 +13,7 @@ import org.hibernate.event.spi.FlushEvent; import org.hibernate.event.spi.FlushEventListener; import org.hibernate.reactive.event.ReactiveFlushEventListener; -import org.hibernate.reactive.logging.impl.Log; -import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.stat.spi.StatisticsImplementor; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; @@ -26,14 +24,13 @@ public class DefaultReactiveFlushEventListener extends AbstractReactiveFlushingEventListener implements ReactiveFlushEventListener, FlushEventListener { - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - @Override public CompletionStage reactiveOnFlush(FlushEvent event) throws HibernateException { final EventSource source = event.getSession(); final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); - if ( persistenceContext.getNumberOfManagedEntities() > 0 || persistenceContext.getCollectionEntriesSize() > 0 ) { + if ( persistenceContext.getNumberOfManagedEntities() > 0 + || persistenceContext.getCollectionEntriesSize() > 0 ) { source.getEventListenerManager().flushStart(); return flushEverythingToExecutions( event ) @@ -51,7 +48,13 @@ public CompletionStage reactiveOnFlush(FlushEvent event) throws HibernateE } } ); } - return voidFuture(); + else if ( ((ReactiveSession) source).getReactiveActionQueue().hasAnyQueuedActions() ) { + // execute any queued unloaded-entity deletions + return performExecutions( source ); + } + else { + return voidFuture(); + } } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveInitializeCollectionEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveInitializeCollectionEventListener.java index c7b15c409..1035075fb 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveInitializeCollectionEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveInitializeCollectionEventListener.java @@ -42,9 +42,10 @@ public void onInitializeCollection(InitializeCollectionEvent event) throws Hiber * called by a collection that wants to initialize itself */ public CompletionStage onReactiveInitializeCollection(InitializeCollectionEvent event) throws HibernateException { - PersistentCollection collection = event.getCollection(); - SessionImplementor source = event.getSession(); - CollectionEntry ce = source.getPersistenceContextInternal().getCollectionEntry( collection ); + final PersistentCollection collection = event.getCollection(); + final SessionImplementor source = event.getSession(); + + final CollectionEntry ce = source.getPersistenceContextInternal().getCollectionEntry( collection ); if ( ce == null ) { throw LOG.collectionWasEvicted(); } @@ -55,50 +56,55 @@ public CompletionStage onReactiveInitializeCollection(InitializeCollection } final CollectionPersister loadedPersister = ce.getLoadedPersister(); + final Object loadedKey = ce.getLoadedKey(); if ( LOG.isTraceEnabled() ) { - LOG.tracev( "Initializing collection {0}", collectionInfoString( loadedPersister, collection, ce.getLoadedKey(), source ) ); + LOG.tracev( + "Initializing collection {0}", + collectionInfoString( loadedPersister, collection, loadedKey, source ) + ); LOG.trace( "Checking second-level cache" ); } - final boolean foundInCache = initializeCollectionFromCache( ce.getLoadedKey(), loadedPersister, collection, source ); + final boolean foundInCache = initializeCollectionFromCache( loadedKey, loadedPersister, collection, source ); if ( foundInCache ) { if ( LOG.isTraceEnabled() ) { LOG.trace( "Collection initialized from cache" ); } return voidFuture(); } - - if ( LOG.isTraceEnabled() ) { - LOG.trace( "Collection not cached" ); + else { + if ( LOG.isTraceEnabled() ) { + LOG.trace( "Collection not cached" ); + } + return ( (ReactiveCollectionPersister) loadedPersister ) + .reactiveInitialize( loadedKey, source ) + .thenApply( list -> { + handlePotentiallyEmptyCollection( collection, source, ce, loadedPersister ); + return list; + } ) + .thenAccept( list -> { + if ( LOG.isTraceEnabled() ) { + LOG.trace( "Collection initialized" ); + } + + final StatisticsImplementor statistics = source.getFactory().getStatistics(); + if ( statistics.isStatisticsEnabled() ) { + statistics.fetchCollection( loadedPersister.getRole() ); + } + } ); } - return ( (ReactiveCollectionPersister) loadedPersister ) - .reactiveInitialize( ce.getLoadedKey(), source ) - .thenApply( list -> { - handlePotentiallyEmptyCollection( collection, source, ce, ce.getLoadedPersister() ); - return list; - } ) - .thenAccept( list -> { - if ( LOG.isTraceEnabled() ) { - LOG.trace( "Collection initialized" ); - } - - final StatisticsImplementor statistics = source.getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.fetchCollection( loadedPersister.getRole() ); - } - } ); } private void handlePotentiallyEmptyCollection( PersistentCollection collection, SessionImplementor source, CollectionEntry ce, - CollectionPersister ceLoadedPersister) { + CollectionPersister loadedPersister) { if ( !collection.wasInitialized() ) { - collection.initializeEmptyCollection( ceLoadedPersister ); + collection.initializeEmptyCollection( loadedPersister ); ResultsHelper.finalizeCollectionLoading( source.getPersistenceContext(), - ceLoadedPersister, + loadedPersister, collection, ce.getLoadedKey(), true @@ -120,10 +126,11 @@ private void handlePotentiallyEmptyCollection( private boolean initializeCollectionFromCache( Object id, CollectionPersister persister, - PersistentCollection collection, + PersistentCollection collection, SessionImplementor source) { - if ( source.getLoadQueryInfluencers().hasEnabledFilters() && persister.isAffectedByEnabledFilters( source ) ) { + if ( source.getLoadQueryInfluencers().hasEnabledFilters() + && persister.isAffectedByEnabledFilters( source ) ) { LOG.trace( "Disregarding cached version (if any) of collection due to enabled filters" ); return false; } @@ -153,7 +160,8 @@ private boolean initializeCollectionFromCache( return false; } - CollectionCacheEntry cacheEntry = (CollectionCacheEntry) persister.getCacheEntryStructure().destructure( ce, factory ); + final CollectionCacheEntry cacheEntry = (CollectionCacheEntry) + persister.getCacheEntryStructure().destructure( ce, factory ); final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); cacheEntry.assemble( collection, persister, persistenceContext.getCollectionOwner( id, persister ) ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java index 5e7003879..053c395af 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java @@ -21,12 +21,11 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.PersistentAttributeInterceptable; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; -import org.hibernate.event.spi.LoadEventListener; +import org.hibernate.event.spi.LoadEventListener;import org.hibernate.event.spi.LoadEventListener.LoadType; import org.hibernate.loader.ast.internal.CacheEntityLoaderHelper; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMappingsList; @@ -82,9 +81,7 @@ public class DefaultReactiveLoadEventListener implements LoadEventListener, Reac * @throws UnexpectedAccessToTheDatabase if it needs to load the entity from the db */ @Override - public void onLoad( - final LoadEvent event, - final LoadEventListener.LoadType loadType) throws HibernateException { + public void onLoad(LoadEvent event, LoadType loadType) throws HibernateException { final EntityPersister persister = getPersister( event ); if ( persister == null ) { @@ -130,10 +127,7 @@ public void onLoad( * @param event The load event to be handled. */ @Override - public CompletionStage reactiveOnLoad( - final LoadEvent event, - final LoadEventListener.LoadType loadType) throws HibernateException { - + public CompletionStage reactiveOnLoad(LoadEvent event, LoadType loadType) throws HibernateException { final ReactiveEntityPersister persister = (ReactiveEntityPersister) getPersister( event ); if ( persister == null ) { throw LOG.unableToLocatePersister( event.getEntityClassName() ); @@ -154,6 +148,7 @@ public CompletionStage reactiveOnLoad( .thenCompose( v -> { // if a pessimistic version increment was requested, we need // to go back to the database immediately and update the row + // we handle this here instead of in DefaultReactivePostLoadEventListener if ( event.getLockMode() == LockMode.PESSIMISTIC_FORCE_INCREMENT ) { // TODO: should we call CachedDomainDataAccess.lockItem() ? return persister.reactiveLock( @@ -164,7 +159,9 @@ public CompletionStage reactiveOnLoad( event.getSession() ); } - return voidFuture(); + else { + return voidFuture(); + } } ); } @@ -172,7 +169,7 @@ private CompletionStage checkId(LoadEvent event, LoadType loadType, Entity final Class idClass = persister.getIdentifierType().getReturnedClass(); if ( idClass != null && !idClass.isInstance( event.getEntityId() ) - && !(event.getEntityId() instanceof DelayedPostInsertIdentifier) ) { + && !( event.getEntityId() instanceof DelayedPostInsertIdentifier ) ) { return checkIdClass( persister, event, loadType, idClass ); } return voidFuture(); @@ -180,21 +177,21 @@ private CompletionStage checkId(LoadEvent event, LoadType loadType, Entity protected EntityPersister getPersister(final LoadEvent event) { final Object instanceToLoad = event.getInstanceToLoad(); + final EventSource source = event.getSession(); if ( instanceToLoad != null ) { //the load() which takes an entity does not pass an entityName event.setEntityClassName( instanceToLoad.getClass().getName() ); - return event.getSession().getEntityPersister( null, instanceToLoad ); + return source.getEntityPersister( null, instanceToLoad ); } else { - return event.getSession().getFactory().getMappingMetamodel() - .findEntityDescriptor( event.getEntityClassName() ); + return source.getFactory().getMappingMetamodel().getEntityDescriptor( event.getEntityClassName() ); } } private CompletionStage doOnLoad( final EntityPersister persister, final LoadEvent event, - final LoadEventListener.LoadType loadType) { + final LoadType loadType) { final EventSource session = event.getSession(); final EntityKey keyToLoad = session.generateEntityKey( event.getEntityId(), persister ); @@ -204,17 +201,15 @@ private CompletionStage doOnLoad( return load( event, persister, keyToLoad, loadType ); } //return a proxy if appropriate - if ( event.getLockMode() == LockMode.NONE ) { - return proxyOrLoad( event, persister, keyToLoad, loadType ); - } - - return lockAndLoad( event, persister, keyToLoad, loadType, session ); + return event.getLockMode() == LockMode.NONE + ? proxyOrLoad( event, persister, keyToLoad, loadType ) + : lockAndLoad( event, persister, keyToLoad, loadType, session ); } private CompletionStage checkIdClass( final EntityPersister persister, final LoadEvent event, - final LoadEventListener.LoadType loadType, + final LoadType loadType, final Class idClass) { // we may have the kooky jpa requirement of allowing find-by-id where // "id" is the "simple pk value" of a dependent objects parent. This @@ -255,9 +250,13 @@ else if ( idMapping instanceof NonAggregatedIdentifierMapping ) { /* * See DefaultLoadEventListener#loadByDerivedIdentitySimplePkValue */ - private CompletionStage loadByDerivedIdentitySimplePkValue(LoadEvent event, LoadEventListener.LoadType options, - EntityPersister dependentPersister, CompositeIdentifierMapping dependentIdType, EntityPersister parentPersister) { - EventSource session = event.getSession(); + private CompletionStage loadByDerivedIdentitySimplePkValue( + LoadEvent event, + LoadType options, + EntityPersister dependentPersister, + CompositeIdentifierMapping dependentIdType, + EntityPersister parentPersister) { + final EventSource session = event.getSession(); final EntityKey parentEntityKey = session.generateEntityKey( event.getEntityId(), parentPersister ); return doLoad( event, parentPersister, parentEntityKey, options ) .thenApply( parent -> { @@ -317,32 +316,25 @@ else if ( isOptionalInstance && optional != event.getInstanceToLoad() ) { * * @return The result of the proxy/load operation. */ - private CompletionStage proxyOrLoad(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadEventListener.LoadType options) { - final EventSource session = event.getSession(); - final SessionFactoryImplementor factory = session.getFactory(); - final boolean traceEnabled = LOG.isTraceEnabled(); - - if ( traceEnabled ) { - LOG.tracev( "Loading entity: {0}", infoString( persister, event.getEntityId(), factory ) ); + private CompletionStage proxyOrLoad(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { + if ( LOG.isTraceEnabled() ) { + LOG.tracev( + "Loading entity: {0}", + infoString( persister, event.getEntityId(), persister.getFactory() ) + ); } // Check for the case where we can use the entity itself as a proxy if ( hasBytecodeProxy( persister, options ) ) { return loadWithBytecodeProxy( event, persister, keyToLoad, options ); } - - if ( persister.hasProxy() ) { + else if ( persister.hasProxy() ) { return loadWithRegularProxy( event, persister, keyToLoad, options ); } - - // return a newly loaded object - return load( event, persister, keyToLoad, options ); - } - - // Copy of DefaultLoadEventLister#hasBytecodeProxy - private static boolean hasBytecodeProxy(EntityPersister persister, LoadType options) { - return options.isAllowProxyCreation() - && persister.getEntityPersister().getBytecodeEnhancementMetadata().isEnhancedForLazyLoading(); + else { + // no proxies, just return a newly loaded object + return load( event, persister, keyToLoad, options ); + } } private static boolean wasDeleted(PersistenceContext persistenceContext, Object existing) { @@ -360,18 +352,41 @@ private CompletionStage loadWithBytecodeProxy(LoadEvent event, EntityPer ? nullFuture() : completedFuture( managed ); } - - // if the entity defines a HibernateProxy factory, see if there is an - // existing proxy associated with the PC - and if so, use it - if ( persister.getRepresentationStrategy().getProxyFactory() != null ) { + else if ( persister.getRepresentationStrategy().getProxyFactory() != null ) { + // we have a HibernateProxy factory, this case is more complicated return loadWithProxyFactory( event, persister, keyToLoad ); } + else if ( !persister.hasSubclasses() ) { + // the entity class has subclasses and there is no HibernateProxy factory + return load( event, persister, keyToLoad, options ); + } + else { + // no HibernateProxy factory, and no subclasses + return completedFuture( createBatchLoadableEnhancedProxy( persister, keyToLoad, session ) ); + } + } - if ( !persister.hasSubclasses() ) { + private CompletionStage loadWithRegularProxy(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { + // This is the case where the proxy is a separate object: + // look for a proxy + final Object proxy = event.getSession().getPersistenceContextInternal().getProxy( keyToLoad ); + if ( proxy != null ) { + // narrow the existing proxy to the type we're looking for + return returnNarrowedProxy( event, persister, keyToLoad, options, proxy ); + } + else if ( options.isAllowProxyCreation() ) { + // return a new proxy + // ORM calls DefaultLoadEventListener#proxyOrCache + return completedFuture( createProxyIfNecessary( event, persister, keyToLoad, options ) ); + } + else { return load( event, persister, keyToLoad, options ); } + } - return completedFuture( createBatchLoadableEnhancedProxy( persister, keyToLoad, session ) ); + private static boolean hasBytecodeProxy(EntityPersister persister, LoadType options) { + return options.isAllowProxyCreation() + && persister.getEntityPersister().getBytecodeEnhancementMetadata().isEnhancedForLazyLoading(); } private static CompletionStage loadWithProxyFactory(LoadEvent event, EntityPersister persister, EntityKey keyToLoad) { @@ -387,18 +402,18 @@ private static CompletionStage loadWithProxyFactory(LoadEvent event, Ent return completedFuture( persistenceContext.narrowProxy( proxy, persister, keyToLoad, null ) ); } - - if ( persister.hasSubclasses() ) { + else if ( persister.hasSubclasses() ) { // specialized handling for entities with subclasses with a HibernateProxy factory // entities with subclasses that define a ProxyFactory can create a HibernateProxy // Maybe we can get it from the cache: Check DefaultLoadEventListener#proxyOrCache return completedFuture( createProxy( event, persister, keyToLoad ) ); } - - return completedFuture( createBatchLoadableEnhancedProxy( persister, keyToLoad, session ) ); + else { + // no existing proxy, and no subclasses + return completedFuture( createBatchLoadableEnhancedProxy( persister, keyToLoad, session ) ); + } } - private static PersistentAttributeInterceptable createBatchLoadableEnhancedProxy( EntityPersister persister, EntityKey keyToLoad, @@ -412,21 +427,6 @@ private static PersistentAttributeInterceptable createBatchLoadableEnhancedProxy return persister.getBytecodeEnhancementMetadata().createEnhancedProxy( keyToLoad, true, session ); } - private CompletionStage loadWithRegularProxy(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { - // look for a proxy - Object proxy = event.getSession().getPersistenceContextInternal().getProxy( keyToLoad ); - if ( proxy != null ) { - return returnNarrowedProxy( event, persister, keyToLoad, options, proxy ); - } - - if ( options.isAllowProxyCreation() ) { - // ORM calls DefaultLoadEventListener#proxyOrCache - return completedFuture( createProxyIfNecessary( event, persister, keyToLoad, options ) ); - } - - return load( event, persister, keyToLoad, options ); - } - private CompletionStage proxyImplementation(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { return load( event, persister, keyToLoad, options ) .thenApply( optional -> { @@ -461,7 +461,7 @@ private CompletionStage returnNarrowedProxy( LoadEvent event, EntityPersister persister, EntityKey keyToLoad, - LoadEventListener.LoadType options, + LoadType options, Object proxy) { if ( LOG.isTraceEnabled() ) { LOG.trace( "Entity proxy found in session cache" ); @@ -501,7 +501,7 @@ private Object createProxyIfNecessary( LoadEvent event, EntityPersister persister, EntityKey keyToLoad, - LoadEventListener.LoadType options) { + LoadType options) { final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal(); final Object existing = persistenceContext.getEntity( keyToLoad ); final boolean traceEnabled = LOG.isTraceEnabled(); @@ -553,7 +553,7 @@ private CompletionStage lockAndLoad( LoadEvent event, EntityPersister persister, EntityKey keyToLoad, - LoadEventListener.LoadType options, + LoadType options, SessionImplementor source) { final boolean canWriteToCache = persister.canWriteToCache(); @@ -610,7 +610,7 @@ private CompletionStage doLoad( LoadEvent event, EntityPersister persister, EntityKey keyToLoad, - LoadEventListener.LoadType options) { + LoadType options) { final EventSource session = event.getSession(); final boolean traceEnabled = LOG.isTraceEnabled(); @@ -621,8 +621,8 @@ private CompletionStage doLoad( ); } - CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = ReactiveCacheEntityLoaderHelper.INSTANCE - .loadFromSessionCache( event, keyToLoad, options ); + final CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = + ReactiveCacheEntityLoaderHelper.INSTANCE.loadFromSessionCache( event, keyToLoad, options ); Object entity = persistenceContextEntry.getEntity(); if ( entity != null ) { return persistenceContextEntry.isManaged() @@ -660,8 +660,7 @@ private CompletionStage doLoad( private void cacheNaturalId(LoadEvent event, EntityPersister persister, EventSource session, Object entity) { if ( entity != null && persister.hasNaturalIdentifier() ) { - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - persistenceContext.getNaturalIdResolutions() + session.getPersistenceContextInternal().getNaturalIdResolutions() .cacheResolutionFromLoad( event.getEntityId(), persister.getNaturalIdMapping().extractNaturalIdFromEntity( entity ), persister diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java index 7d22d8068..4a2de4d2b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java @@ -25,6 +25,7 @@ import org.hibernate.event.spi.LockEvent; import org.hibernate.event.spi.LockEventListener; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.engine.ReactiveActionQueue; import org.hibernate.reactive.engine.impl.Cascade; import org.hibernate.reactive.engine.impl.CascadingActions; import org.hibernate.reactive.engine.impl.ForeignKeys; @@ -59,7 +60,7 @@ public CompletionStage reactiveOnLock(LockEvent event) throws HibernateExc LOG.explicitSkipLockedLockCombo(); } - EventSource source = event.getSession(); + final EventSource source = event.getSession(); boolean detached = event.getEntityName() != null ? !source.contains( event.getEntityName(), event.getObject() ) @@ -80,35 +81,35 @@ public CompletionStage reactiveOnLock(LockEvent event) throws HibernateExc private CompletionStage reactiveOnLock(LockEvent event, Object entity) { - SessionImplementor source = event.getSession(); - PersistenceContext persistenceContext = source.getPersistenceContextInternal(); - - EntityEntry entry = persistenceContext.getEntry(entity); - CompletionStage stage; - if (entry==null) { - final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity); - final Object id = persister.getIdentifier(entity, source); - stage = ForeignKeys.isNotTransient( event.getEntityName(), entity, Boolean.FALSE, source).thenApply( - trans -> { - if (!trans) { - throw new TransientObjectException( - "cannot lock an unsaved transient instance: " + - persister.getEntityName() - ); - } + final SessionImplementor source = event.getSession(); + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); - EntityEntry e = reassociate(event, entity, id, persister); - cascadeOnLock(event, persister, entity); - return e; - } - ); + final EntityEntry entry = persistenceContext.getEntry(entity); + final CompletionStage stage; + if ( entry==null ) { + final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); + final Object id = persister.getIdentifier( entity, source ); + stage = ForeignKeys.isNotTransient( event.getEntityName(), entity, Boolean.FALSE, source) + .thenApply( trans -> { + if (!trans) { + throw new TransientObjectException( + "cannot lock an unsaved transient instance: " + + persister.getEntityName() + ); + } + + final EntityEntry e = reassociate( event, entity, id, persister ); + cascadeOnLock( event, persister, entity ); + return e; + } + ); } else { stage = completedFuture( entry ); } - return stage.thenCompose( e -> upgradeLock(entity, e, event.getLockOptions(), event.getSession() ) ); + return stage.thenCompose( e -> upgradeLock( entity, e, event.getLockOptions(), event.getSession() ) ); } private void cascadeOnLock(LockEvent event, EntityPersister persister, Object entity) { @@ -138,11 +139,12 @@ private void cascadeOnLock(LockEvent event, EntityPersister persister, Object en * @param lockOptions contains the requested lock mode. * @param source The session which is the source of the event being processed. */ - protected CompletionStage upgradeLock(Object object, EntityEntry entry, - LockOptions lockOptions, - EventSource source) { - - LockMode requestedLockMode = lockOptions.getLockMode(); + protected CompletionStage upgradeLock( + Object object, + EntityEntry entry, + LockOptions lockOptions, + EventSource source) { + final LockMode requestedLockMode = lockOptions.getLockMode(); if ( requestedLockMode.greaterThan( entry.getLockMode() ) ) { // The user requested a "greater" (i.e. more restrictive) form of // pessimistic lock @@ -163,15 +165,14 @@ protected CompletionStage upgradeLock(Object object, EntityEntry entry, ); } - switch (requestedLockMode) { + final ReactiveActionQueue actionQueue = ((ReactiveSession) source).getReactiveActionQueue(); + switch ( requestedLockMode ) { case OPTIMISTIC: - ( (ReactiveSession) source ).getReactiveActionQueue() - .registerProcess( new ReactiveEntityVerifyVersionProcess(object) ); + actionQueue.registerProcess( new ReactiveEntityVerifyVersionProcess( object ) ); entry.setLockMode( requestedLockMode ); return voidFuture(); case OPTIMISTIC_FORCE_INCREMENT: - ( (ReactiveSession) source ).getReactiveActionQueue() - .registerProcess( new ReactiveEntityIncrementVersionProcess(object) ); + actionQueue.registerProcess( new ReactiveEntityIncrementVersionProcess( object ) ); entry.setLockMode( requestedLockMode ); return voidFuture(); default: diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveMergeEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveMergeEventListener.java index 1995f47ef..33dafbc2b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveMergeEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveMergeEventListener.java @@ -27,13 +27,11 @@ import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.SelfDirtinessTracker; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.event.internal.EntityState; import org.hibernate.event.internal.EventUtil; import org.hibernate.event.internal.WrapVisitor; import org.hibernate.event.spi.EntityCopyObserver; -import org.hibernate.event.spi.EntityCopyObserverFactory; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.MergeContext; import org.hibernate.event.spi.MergeEvent; @@ -57,9 +55,10 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker; +import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTracker; -import static org.hibernate.event.internal.EntityState.DETACHED; +import static org.hibernate.event.internal.EntityState.getEntityState; import static org.hibernate.reactive.util.impl.CompletionStages.loop; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; import static org.hibernate.type.ForeignKeyDirection.FROM_PARENT; @@ -95,20 +94,19 @@ protected Map getMergeMap(MergeContext context) { */ @Override public CompletionStage reactiveOnMerge(MergeEvent event) throws HibernateException { - final EntityCopyObserver entityCopyObserver = createEntityCopyObserver( event.getSession().getFactory() ); - final MergeContext mergeContext = new MergeContext( event.getSession(), entityCopyObserver ); + final EventSource session = event.getSession(); + final EntityCopyObserver entityCopyObserver = createEntityCopyObserver( session ); + final MergeContext mergeContext = new MergeContext( session, entityCopyObserver ); return reactiveOnMerge( event, mergeContext ) - .thenAccept( v -> entityCopyObserver.topLevelMergeComplete( event.getSession() ) ) + .thenAccept( v -> entityCopyObserver.topLevelMergeComplete( session ) ) .whenComplete( (v, e) -> { entityCopyObserver.clear(); mergeContext.clear(); } ); } - private EntityCopyObserver createEntityCopyObserver(SessionFactoryImplementor sessionFactory) { - return sessionFactory.getServiceRegistry() - .getService( EntityCopyObserverFactory.class ) - .createEntityCopyObserver(); + private EntityCopyObserver createEntityCopyObserver(final EventSource session) { + return session.getFactory().getFastSessionServices().entityCopyObserverFactory.createEntityCopyObserver(); } /** @@ -119,23 +117,20 @@ private EntityCopyObserver createEntityCopyObserver(SessionFactoryImplementor se @Override public CompletionStage reactiveOnMerge(MergeEvent event, MergeContext copiedAlready) throws HibernateException { - final EventSource source = event.getSession(); final Object original = event.getOriginal(); - // NOTE : `original` is the value being merged - if ( original != null ) { + final EventSource source = event.getSession(); final Object entity; - if ( original instanceof HibernateProxy ) { - LazyInitializer li = ( (HibernateProxy) original ).getHibernateLazyInitializer(); - if ( li.isUninitialized() ) { + final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( original ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.isUninitialized() ) { LOG.trace( "Ignoring uninitialized proxy" ); - event.setResult( source.load( li.getEntityName(), li.getIdentifier() ) ); - //EARLY EXIT! + event.setResult( source.load( lazyInitializer.getEntityName(), lazyInitializer.getInternalIdentifier() ) ); return voidFuture(); } else { - entity = li.getImplementation(); + entity = lazyInitializer.getImplementation(); } } else if ( isPersistentAttributeInterceptable( original ) ) { @@ -157,76 +152,78 @@ else if ( isPersistentAttributeInterceptable( original ) ) { entity = original; } - if ( copiedAlready.containsKey( entity ) && ( copiedAlready.isOperatedOn( entity ) ) ) { - LOG.trace( "Already in merge process" ); - event.setResult( entity ); - } - else { - if ( copiedAlready.containsKey( entity ) ) { - LOG.trace( "Already in copyCache; setting in merge process" ); - copiedAlready.setOperatedOn( entity, true ); - } - event.setEntity( entity ); - EntityState entityState = null; - - // Check the persistence context for an entry relating to this - // entity to be merged... - final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); - EntityEntry entry = persistenceContext.getEntry( entity ); - if ( entry == null ) { - EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - Object id = persister.getIdentifier( entity, source ); - if ( id != null ) { - final EntityKey key = source.generateEntityKey( id, persister ); - final Object managedEntity = persistenceContext.getEntity( key ); - entry = persistenceContext.getEntry( managedEntity ); - if ( entry != null ) { - // we have specialized case of a detached entity from the - // perspective of the merge operation. Specifically, we - // have an incoming entity instance which has a corresponding - // entry in the current persistence context, but registered - // under a different entity instance - entityState = DETACHED; - } - } - } + return doMerge( event, copiedAlready, entity ); - if ( entityState == null ) { - entityState = EntityState.getEntityState( entity, event.getEntityName(), entry, source, false ); - } + } - switch ( entityState ) { - case DETACHED: - return entityIsDetached( event, copiedAlready ); - case TRANSIENT: - return entityIsTransient( event, copiedAlready ); - case PERSISTENT: - return entityIsPersistent( event, copiedAlready ); - default: //DELETED - throw new ObjectDeletedException( - "deleted instance passed to merge", - null, - EventUtil.getLoggableName( event.getEntityName(), entity ) - ); - } + return voidFuture(); + } + + private CompletionStage doMerge(MergeEvent event, MergeContext copiedAlready, Object entity) { + if ( copiedAlready.containsKey( entity ) && ( copiedAlready.isOperatedOn( entity ) ) ) { + LOG.trace( "Already in merge process" ); + event.setResult( entity ); + return voidFuture(); + } + else { + if ( copiedAlready.containsKey( entity ) ) { + LOG.trace( "Already in copyCache; setting in merge process" ); + copiedAlready.setOperatedOn( entity, true ); } + event.setEntity( entity ); + return merge( event, copiedAlready, entity ); + } + } + private CompletionStage merge(MergeEvent event, MergeContext copiedAlready, Object entity) { + switch ( entityState( event, entity ) ) { + case DETACHED: + return entityIsDetached( event, copiedAlready ); + case TRANSIENT: + return entityIsTransient( event, copiedAlready ); + case PERSISTENT: + return entityIsPersistent( event, copiedAlready ); + default: //DELETED + throw new ObjectDeletedException( + "deleted instance passed to merge", + null, + EventUtil.getLoggableName( event.getEntityName(), entity) + ); } + } - return voidFuture(); + private static EntityState entityState(MergeEvent event, Object entity) { + final EventSource source = event.getSession(); + // Check the persistence context for an entry relating to this + // entity to be merged... + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); + EntityEntry entry = persistenceContext.getEntry( entity ); + if ( entry == null ) { + EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); + Object id = persister.getIdentifier( entity, source ); + if ( id != null ) { + final Object managedEntity = persistenceContext.getEntity( source.generateEntityKey( id, persister ) ); + entry = persistenceContext.getEntry( managedEntity ); + if ( entry != null ) { + // we have a special case of a detached entity from the + // perspective of the merge operation. Specifically, we have + // an incoming entity instance which has a corresponding + // entry in the current persistence context, but registered + // under a different entity instance + return EntityState.DETACHED; + } + } + } + return getEntityState( entity, event.getEntityName(), entry, source, false ); } protected CompletionStage entityIsPersistent(MergeEvent event, MergeContext copyCache) { LOG.trace( "Ignoring persistent instance" ); - //TODO: check that entry.getIdentifier().equals(requestedId) - final Object entity = event.getEntity(); final EventSource source = event.getSession(); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - copyCache.put( entity, entity, true ); //before cascade! - return cascadeOnMerge( source, persister, entity, copyCache ) .thenCompose( v -> fetchAndCopyValues( persister, entity, entity, source, copyCache ) ) .thenAccept( v -> event.setResult( entity ) ); @@ -330,14 +327,10 @@ protected CompletionStage entityIsDetached(MergeEvent event, MergeContext final Object entity = event.getEntity(); final EventSource source = event.getSession(); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - final String entityName = persister.getEntityName(); - Object requestedId = event.getRequestedId(); Object id = getDetachedEntityId( event, entity, persister ); - // we must clone embedded composite identifiers, or we will get back the same instance that we pass in final Object clonedIdentifier = persister.getIdentifierType().deepCopy( id, source.getFactory() ); - return source.getLoadQueryInfluencers() .fromInternalFetchProfile( CascadingFetchProfile.MERGE, () -> source.unwrap( ReactiveSession.class ) .reactiveGet( (Class) persister.getMappedClass(), clonedIdentifier ) @@ -353,20 +346,21 @@ protected CompletionStage entityIsDetached(MergeEvent event, MergeContext // really persistent return entityIsTransient( event, copyCache ); } - - // before cascade! - copyCache.put( entity, result, true ); - final Object target = targetEntity( event, entity, persister, id, result ); - // cascade first, so that all unsaved objects get their - // copy created before we actually copy - return cascadeOnMerge( source, persister, entity, copyCache ) - .thenCompose( v -> fetchAndCopyValues( persister, entity, target, source, copyCache ) ) - .thenAccept( v -> { - // copyValues() (called by fetchAndCopyValues) works by reflection, - // so explicitly mark the entity instance dirty - markInterceptorDirty( entity, target ); - event.setResult( result ); - } ); + else { + // before cascade! + copyCache.put( entity, result, true ); + final Object target = targetEntity( event, entity, persister, id, result ); + // cascade first, so that all unsaved objects get their + // copy created before we actually copy + return cascadeOnMerge( source, persister, entity, copyCache ) + .thenCompose( v -> fetchAndCopyValues( persister, entity, target, source, copyCache ) ) + .thenAccept( v -> { + // copyValues() (called by fetchAndCopyValues) works by reflection, + // so explicitly mark the entity instance dirty + markInterceptorDirty( entity, target ); + event.setResult( result ); + } ); + } } ); } @@ -399,18 +393,17 @@ else if ( isVersionChanged( entity, source, persister, target ) ) { private static Object getDetachedEntityId(MergeEvent event, Object entity, EntityPersister persister) { final EventSource source = event.getSession(); final Object id = event.getRequestedId(); - if ( id == null ) { return persister.getIdentifier( entity, source ); } - - // check that entity id = requestedId - Object entityId = persister.getIdentifier( entity, source ); - if ( !persister.getIdentifierType().isEqual( id, entityId, source.getFactory() ) ) { - throw LOG.mergeRequestedIdNotMatchingIdOfPassedEntity(); + else { + // check that entity id = requestedId + final Object entityId = persister.getIdentifier( entity, source ); + if ( !persister.getIdentifierType().isEqual( id, entityId, source.getFactory() ) ) { + throw LOG.mergeRequestedIdNotMatchingIdOfPassedEntity(); + } + return id; } - - return id; } private static Object unproxyManagedForDetachedMerging( @@ -418,15 +411,17 @@ private static Object unproxyManagedForDetachedMerging( Object managed, EntityPersister persister, EventSource source) { - if ( managed instanceof HibernateProxy ) { + if ( isHibernateProxy( managed ) ) { return source.getPersistenceContextInternal().unproxy( managed ); } if ( isPersistentAttributeInterceptable( incoming ) && persister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ) { - final PersistentAttributeInterceptor incomingInterceptor = asPersistentAttributeInterceptable( incoming ).$$_hibernate_getInterceptor(); - final PersistentAttributeInterceptor managedInterceptor = asPersistentAttributeInterceptable( managed ).$$_hibernate_getInterceptor(); + final PersistentAttributeInterceptor incomingInterceptor = + asPersistentAttributeInterceptable( incoming ).$$_hibernate_getInterceptor(); + final PersistentAttributeInterceptor managedInterceptor = + asPersistentAttributeInterceptable( managed ).$$_hibernate_getInterceptor(); // todo - do we need to specially handle the case where both `incoming` and `managed` are initialized, but // with different attributes initialized? @@ -463,27 +458,29 @@ private static void markInterceptorDirty(final Object entity, final Object targe } private static boolean isVersionChanged(Object entity, EventSource source, EntityPersister persister, Object target) { - if ( !persister.isVersioned() ) { + if ( persister.isVersioned() ) { + // for merging of versioned entities, we consider the version having + // been changed only when: + // 1) the two version values are different; + // *AND* + // 2) The target actually represents database state! + // + // This second condition is a special case which allows + // an entity to be merged during the same transaction + // (though during a seperate operation) in which it was + // originally persisted/saved + boolean changed = !persister.getVersionType().isSame( + persister.getVersion( target ), + persister.getVersion( entity ) + ); + + // TODO : perhaps we should additionally require that the incoming entity + // version be equivalent to the defined unsaved-value? + return changed && existsInDatabase( target, source, persister ); + } + else { return false; } - // for merging of versioned entities, we consider the version having - // been changed only when: - // 1) the two version values are different; - // *AND* - // 2) The target actually represents database state! - // - // This second condition is a special case which allows - // an entity to be merged during the same transaction - // (though during a seperate operation) in which it was - // originally persisted/saved - boolean changed = !persister.getVersionType().isSame( - persister.getVersion( target ), - persister.getVersion( entity ) - ); - - // TODO : perhaps we should additionally require that the incoming entity - // version be equivalent to the defined unsaved-value? - return changed && existsInDatabase( target, source, persister ); } private static boolean existsInDatabase(Object entity, EventSource source, EntityPersister persister) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePersistEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePersistEventListener.java index 53ae160b0..2b5e04206 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePersistEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePersistEventListener.java @@ -31,6 +31,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import static org.hibernate.event.internal.EntityState.getEntityState; import static org.hibernate.pretty.MessageHelper.infoString; import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; @@ -75,36 +76,54 @@ public void onPersist(PersistEvent event, PersistContext createdAlready) throws */ @Override public CompletionStage reactiveOnPersist(PersistEvent event, PersistContext createCache) { - final SessionImplementor source = event.getSession(); final Object object = event.getObject(); - final Object entity; - if ( object instanceof HibernateProxy ) { - LazyInitializer li = ( (HibernateProxy) object ).getHibernateLazyInitializer(); - if ( li.isUninitialized() ) { - if ( li.getSession() == source ) { - return voidFuture(); //NOTE EARLY EXIT! - } - else { - return failedFuture( new PersistentObjectException( "uninitialized proxy passed to persist()" ) ); - } + final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( object ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.isUninitialized() ) { + return lazyInitializer.getSession() == event.getSession() + ? voidFuture() + : failedFuture( new PersistentObjectException( "uninitialized proxy passed to persist()" ) ); + } + else { + return persist( event, createCache, lazyInitializer.getImplementation() ); } - entity = li.getImplementation(); } else { - entity = object; + return persist( event, createCache, object ); } + } - final String entityName; - if ( event.getEntityName() != null ) { - entityName = event.getEntityName(); - } - else { - entityName = source.bestGuessEntityName( entity ); - event.setEntityName( entityName ); + private CompletionStage persist(PersistEvent event, PersistContext createCache, Object entity) { + final SessionImplementor source = event.getSession(); + final EntityEntry entityEntry = source.getPersistenceContextInternal().getEntry(entity); + final String entityName = entityName( event, entity, entityEntry ); + switch ( entityState( event, entity, entityName, entityEntry ) ) { + case DETACHED: + return failedFuture( new PersistentObjectException( + "detached entity passed to persist: " + + EventUtil.getLoggableName(event.getEntityName(), entity) + ) ); + case PERSISTENT: + return entityIsPersistent(event, createCache); + case TRANSIENT: + return entityIsTransient(event, createCache); + case DELETED: + entityEntry.setStatus( Status.MANAGED ); + entityEntry.setDeletedState( null ); + event.getSession().getActionQueue().unScheduleDeletion( entityEntry, event.getObject() ); + return entityIsDeleted( event, createCache ); + default: + return failedFuture( new ObjectDeletedException( + "deleted entity passed to persist", + null, + EventUtil.getLoggableName( event.getEntityName(), entity ) + ) ); } + } - final EntityEntry entityEntry = source.getPersistenceContextInternal().getEntry( entity ); - EntityState entityState = EntityState.getEntityState( entity, entityName, entityEntry, source, true ); + private static EntityState entityState(PersistEvent event, Object entity, String entityName, EntityEntry entityEntry) { + final EventSource source = event.getSession(); + EntityState entityState = getEntityState( entity, entityName, entityEntry, source, true ); if ( entityState == EntityState.DETACHED ) { // JPA 2, in its version of a "foreign generated", allows the id attribute value // to be manually set by the user, even though this manual value is irrelevant. @@ -115,59 +134,38 @@ public CompletionStage reactiveOnPersist(PersistEvent event, PersistContex // entity state again. // NOTE: entityEntry must be null to get here, so we cannot use any of its values - final EntityPersister persister = source.getFactory() - .getRuntimeMetamodels() - .getMappingMetamodel() - .getEntityDescriptor( entityName ); - if (persister.getIdentifierGenerator() instanceof ForeignGenerator) { + final EntityPersister persister = source.getFactory().getMappingMetamodel() + .getEntityDescriptor(entityName); + if ( persister.getGenerator() instanceof ForeignGenerator ) { if ( LOG.isDebugEnabled() && persister.getIdentifier( entity, source ) != null ) { LOG.debug( "Resetting entity id attribute to null for foreign generator" ); } persister.setIdentifier( entity, null, source ); - entityState = EntityState.getEntityState( entity, entityName, entityEntry, source, true ); + entityState = getEntityState( entity, entityName, entityEntry, source, true ); } } + return entityState; + } - switch ( entityState ) { - case DETACHED: { - return failedFuture( new PersistentObjectException( - "detached entity passed to persist: " + - EventUtil.getLoggableName( event.getEntityName(), entity ) - ) ); - } - case PERSISTENT: { - return entityIsPersistent( event, createCache ); - } - case TRANSIENT: { - return entityIsTransient( event, createCache ); - } - case DELETED: { - entityEntry.setStatus( Status.MANAGED ); - entityEntry.setDeletedState( null ); - event.getSession().getActionQueue().unScheduleDeletion( entityEntry, event.getObject() ); - return entityIsDeleted( event, createCache ); - } - default: { - return failedFuture( new ObjectDeletedException( - "deleted entity passed to persist", - null, - EventUtil.getLoggableName( event.getEntityName(), entity ) - ) ); - } + private static String entityName(PersistEvent event, Object entity, EntityEntry entityEntry) { + if ( event.getEntityName() != null ) { + return event.getEntityName(); + } + else { + // changes event.entityName by side effect! + final String entityName = event.getSession().bestGuessEntityName( entity, entityEntry ); + event.setEntityName( entityName ); + return entityName; } } protected CompletionStage entityIsPersistent(PersistEvent event, PersistContext createCache) { LOG.trace( "Ignoring persistent instance" ); final EventSource source = event.getSession(); - //TODO: check that entry.getIdentifier().equals(requestedId) - final Object entity = source.getPersistenceContextInternal().unproxy( event.getObject() ); - final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - return createCache.add( entity ) - ? justCascade( createCache, source, entity, persister ) + ? justCascade( createCache, source, entity, source.getEntityPersister( event.getEntityName(), entity ) ) : voidFuture(); } @@ -185,10 +183,8 @@ private CompletionStage justCascade(PersistContext createCache, EventSourc */ protected CompletionStage entityIsTransient(PersistEvent event, PersistContext createCache) { LOG.trace( "Saving transient instance" ); - final EventSource source = event.getSession(); final Object entity = source.getPersistenceContextInternal().unproxy( event.getObject() ); - return createCache.add( entity ) ? reactiveSaveWithGeneratedId( entity, event.getEntityName(), createCache, source, false ) : voidFuture(); @@ -196,17 +192,14 @@ protected CompletionStage entityIsTransient(PersistEvent event, PersistCon private CompletionStage entityIsDeleted(PersistEvent event, PersistContext createCache) { final EventSource source = event.getSession(); - final Object entity = source.getPersistenceContextInternal().unproxy( event.getObject() ); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - if ( LOG.isTraceEnabled() ) { LOG.tracef( "un-scheduling entity deletion [%s]", infoString( persister, persister.getIdentifier( entity, source ), source.getFactory() ) ); } - return createCache.add( entity ) ? justCascade( createCache, source, entity, persister ) : voidFuture(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePostLoadEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePostLoadEventListener.java index 8edc58595..49c62c182 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePostLoadEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactivePostLoadEventListener.java @@ -6,20 +6,21 @@ package org.hibernate.reactive.event.impl; import org.hibernate.AssertionFailure; -import org.hibernate.LockMode; import org.hibernate.classic.Lifecycle; import org.hibernate.engine.spi.EntityEntry; -import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.PostLoadEvent; import org.hibernate.event.spi.PostLoadEventListener; import org.hibernate.jpa.event.spi.CallbackRegistry; import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; +import org.hibernate.reactive.engine.ReactiveActionQueue; import org.hibernate.reactive.engine.impl.ReactiveEntityIncrementVersionProcess; import org.hibernate.reactive.engine.impl.ReactiveEntityVerifyVersionProcess; import org.hibernate.reactive.session.ReactiveSession; /** - * We do 2 things here:
    + * We do two things here: + *
      *
    • Call {@link Lifecycle} interface if necessary
    • *
    • Perform needed {@link EntityEntry#getLockMode()} related processing
    • *
    @@ -41,20 +42,23 @@ public void onPostLoad(PostLoadEvent event) { callbackRegistry.postLoad( entity ); - final SessionImplementor session = event.getSession(); + final EventSource session = event.getSession(); final EntityEntry entry = session.getPersistenceContextInternal().getEntry( entity ); if ( entry == null ) { throw new AssertionFailure( "possible non-threadsafe access to the session" ); } - LockMode lockMode = entry.getLockMode(); - if ( LockMode.OPTIMISTIC_FORCE_INCREMENT.equals( lockMode ) ) { - ((ReactiveSession) session).getReactiveActionQueue() - .registerProcess( new ReactiveEntityIncrementVersionProcess( entity ) ); - } - else if ( LockMode.OPTIMISTIC.equals( lockMode ) ) { - ((ReactiveSession) session).getReactiveActionQueue() - .registerProcess( new ReactiveEntityVerifyVersionProcess( entity ) ); + final ReactiveActionQueue actionQueue = ((ReactiveSession) session).getReactiveActionQueue(); + switch ( entry.getLockMode() ) { +// case PESSIMISTIC_FORCE_INCREMENT: + // This case is handled by DefaultReactiveLoadEventListener + case OPTIMISTIC_FORCE_INCREMENT: + actionQueue.registerProcess( new ReactiveEntityIncrementVersionProcess( entity ) ); + break; + case OPTIMISTIC: + actionQueue.registerProcess( new ReactiveEntityVerifyVersionProcess( entity ) ); + break; } } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java index 10a3167d5..4303aec7c 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java @@ -32,7 +32,7 @@ import org.hibernate.event.spi.RefreshEvent; import org.hibernate.event.spi.RefreshEventListener; import org.hibernate.loader.ast.spi.CascadingFetchProfile; -import org.hibernate.metamodel.spi.MetamodelImplementor; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.engine.impl.Cascade; @@ -75,8 +75,7 @@ public void onRefresh(RefreshEvent event, RefreshContext refreshedAlready) throw */ @Override public CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshContext refreshedAlready) { - - EventSource source = event.getSession(); + final EventSource source = event.getSession(); boolean detached = event.getEntityName() != null ? !source.contains( event.getEntityName(), event.getObject() ) @@ -91,8 +90,8 @@ public CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshContex } private CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshContext refreshedAlready, Object entity) { - EventSource source = event.getSession(); - PersistenceContext persistenceContext = source.getPersistenceContextInternal(); + final EventSource source = event.getSession(); + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); if ( !refreshedAlready.add( entity) ) { LOG.trace( "Already refreshed" ); @@ -100,14 +99,12 @@ private CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshConte } final EntityEntry entry = persistenceContext.getEntry( entity ); + final EntityPersister persister; final Object id; - if ( entry == null ) { - persister = source.getEntityPersister( - event.getEntityName(), - entity - ); //refresh() does not pass an entityName + //refresh() does not pass an entityName + persister = source.getEntityPersister( event.getEntityName(), entity ); id = persister.getIdentifier( entity, event.getSession() ); if ( LOG.isTraceEnabled() ) { LOG.tracev( @@ -141,55 +138,59 @@ private CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshConte } // cascade the refresh prior to refreshing this entity - refreshedAlready.add( entity ); - - return cascadeRefresh(source, persister, entity, refreshedAlready) + return cascadeRefresh( source, persister, entity, refreshedAlready ) .thenCompose( v -> { if ( entry != null ) { final EntityKey key = source.generateEntityKey( id, persister ); persistenceContext.removeEntity( key ); if ( persister.hasCollections() ) { - new EvictVisitor(source, entity ).process( entity, persister ); + new EvictVisitor( source, entity ).process( entity, persister ); } } - if ( persister.canWriteToCache() ) { - Object previousVersion = null; - if ( persister.isVersionPropertyGenerated() ) { - // we need to grab the version value from the entity, otherwise - // we have issues with generated-version entities that may have - // multiple actions queued during the same flush - previousVersion = persister.getVersion( entity ); - } - final EntityDataAccess cache = persister.getCacheAccessStrategy(); - final Object ck = cache.generateCacheKey( - id, - persister, - source.getFactory(), - source.getTenantIdentifier() - ); - final SoftLock lock = cache.lockItem(source, ck, previousVersion ); - cache.remove(source, ck ); - source.getActionQueue().registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); - } + evictEntity( entity, persister, id, source ); + evictCachedCollections( persister, id, source ); - evictCachedCollections( persister, id, source); - - CompletableFuture refresh = new CompletableFuture<>(); + final CompletableFuture refresh = new CompletableFuture<>(); source.getLoadQueryInfluencers() - .fromInternalFetchProfile( CascadingFetchProfile.REFRESH, () -> doRefresh( event, source, entity, entry, persister, id, persistenceContext ) - .whenComplete( (unused, throwable) -> { - if ( throwable == null ) { - refresh.complete( null ); - } - else { - refresh.completeExceptionally( throwable ); - } - } ) ); + .fromInternalFetchProfile( + CascadingFetchProfile.REFRESH, + () -> doRefresh( event, source, entity, entry, persister, id, persistenceContext ) + .whenComplete( (unused, throwable) -> { + if ( throwable == null ) { + refresh.complete( null ); + } + else { + refresh.completeExceptionally( throwable ); + } + } ) + ); return refresh; } ); } + private static void evictEntity(Object entity, EntityPersister persister, Object id, EventSource source) { + if ( persister.canWriteToCache() ) { + Object previousVersion = null; + if ( persister.isVersionPropertyGenerated() ) { + // we need to grab the version value from the entity, otherwise + // we have issues with generated-version entities that may have + // multiple actions queued during the same flush + previousVersion = persister.getVersion( entity ); + } + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + final Object ck = cache.generateCacheKey( + id, + persister, + source.getFactory(), + source.getTenantIdentifier() + ); + final SoftLock lock = cache.lockItem( source, ck, previousVersion ); + cache.remove(source, ck ); + source.getActionQueue().registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); + } + } + private static CompletionStage doRefresh( RefreshEvent event, EventSource source, @@ -198,23 +199,19 @@ private static CompletionStage doRefresh( EntityPersister persister, Object id, PersistenceContext persistenceContext) { - // Handle the requested lock-mode (if one) in relation to the entry's (if one) current lock-mode - LockOptions lockOptionsToUse = event.getLockOptions(); - final LockMode requestedLockMode = lockOptionsToUse.getLockMode(); final LockMode postRefreshLockMode; - if ( entry != null ) { final LockMode currentLockMode = entry.getLockMode(); if ( currentLockMode.greaterThan( requestedLockMode ) ) { // the requested lock-mode is less restrictive than the current one // - pass along the current lock-mode (after accounting for WRITE) - lockOptionsToUse = LockOptions.copy( event.getLockOptions(), new LockOptions() ); - if ( currentLockMode == LockMode.WRITE || - currentLockMode == LockMode.PESSIMISTIC_WRITE || - currentLockMode == LockMode.PESSIMISTIC_READ ) { + lockOptionsToUse = event.getLockOptions().makeCopy(); + if ( currentLockMode == LockMode.WRITE + || currentLockMode == LockMode.PESSIMISTIC_WRITE + || currentLockMode == LockMode.PESSIMISTIC_READ ) { // our transaction should already hold the exclusive lock on // the underlying row - so READ should be sufficient. // @@ -223,7 +220,6 @@ private static CompletionStage doRefresh( // WRITE specially because the Loader/Locker mechanism does not allow for WRITE // locks lockOptionsToUse.setLockMode( LockMode.READ ); - // and prepare to reset the entry lock-mode to the previous lock mode after // the refresh completes postRefreshLockMode = currentLockMode; @@ -247,7 +243,7 @@ private static CompletionStage doRefresh( if ( result != null ) { // apply `postRefreshLockMode`, if needed if ( postRefreshLockMode != null ) { - // if we get here, there was a previous entry and we need to re-set its lock-mode + // if we get here, there was a previous entry, and we need to re-set its lock-mode // - however, the refresh operation actually creates a new entry, so get it persistenceContext.getEntry( result ).setLockMode( postRefreshLockMode ); } @@ -290,10 +286,11 @@ private void evictCachedCollections(Type[] types, Object id, EventSource source) throws HibernateException { final ActionQueue actionQueue = source.getActionQueue(); final SessionFactoryImplementor factory = source.getFactory(); - final MetamodelImplementor metamodel = factory.getMetamodel(); + final MappingMetamodelImplementor metamodel = factory.getRuntimeMetamodels().getMappingMetamodel(); for ( Type type : types ) { if ( type.isCollectionType() ) { - CollectionPersister collectionPersister = metamodel.collectionPersister( ( (CollectionType) type ).getRole() ); + final String role = ((CollectionType) type).getRole(); + final CollectionPersister collectionPersister = metamodel.getCollectionDescriptor( role ); if ( collectionPersister.hasCache() ) { final CollectionDataAccess cache = collectionPersister.getCacheAccessStrategy(); final Object ck = cache.generateCacheKey( @@ -308,8 +305,8 @@ private void evictCachedCollections(Type[] types, Object id, EventSource source) } } else if ( type.isComponentType() ) { - CompositeType actype = (CompositeType) type; - evictCachedCollections( actype.getSubtypes(), id, source ); + final CompositeType compositeType = (CompositeType) type; + evictCachedCollections( compositeType.getSubtypes(), id, source ); } } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveResolveNaturalIdEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveResolveNaturalIdEventListener.java index 0ddb2d69c..0cfb69545 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveResolveNaturalIdEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveResolveNaturalIdEventListener.java @@ -55,17 +55,23 @@ public CompletionStage onReactiveResolveNaturalId(ResolveNaturalIdEvent ev * @return The loaded entity, or null. */ protected CompletionStage resolveNaturalId(ResolveNaturalIdEvent event) { - EntityPersister persister = event.getEntityPersister(); + final EntityPersister persister = event.getEntityPersister(); if ( LOG.isTraceEnabled() ) { - LOG.tracev( "Attempting to resolve: {0}#{1}", infoString( persister ), event.getNaturalIdValues() + LOG.tracev( + "Attempting to resolve: {0}#{1}", + infoString( persister ), + event.getNaturalIdValues() ); } - Object entityId = resolveFromCache( event ); + final Object entityId = resolveFromCache( event ); if ( entityId != null ) { if ( LOG.isTraceEnabled() ) { - LOG.tracev( "Resolved object in cache: {0}#{1}", infoString( persister ), event.getNaturalIdValues() ); + LOG.tracev( + "Resolved object in cache: {0}#{1}", + infoString( persister ), + event.getNaturalIdValues() ); } return completedFuture( entityId ); } @@ -100,25 +106,25 @@ protected Object resolveFromCache(ResolveNaturalIdEvent event) { * * @return The object loaded from the datasource, or null if not found. */ - protected CompletionStage loadFromDatasource(final ResolveNaturalIdEvent event) { - EventSource session = event.getSession(); - StatisticsImplementor statistics = session.getFactory().getStatistics(); - boolean statisticsEnabled = statistics.isStatisticsEnabled(); - long startTime = statisticsEnabled ? System.nanoTime() : 0; + protected CompletionStage loadFromDatasource(ResolveNaturalIdEvent event) { + final EventSource session = event.getSession(); + final EntityPersister entityPersister = event.getEntityPersister(); + final StatisticsImplementor statistics = session.getFactory().getStatistics(); + final boolean statisticsEnabled = statistics.isStatisticsEnabled(); + final long startTime = statisticsEnabled ? System.nanoTime() : 0; - EntityPersister entityPersister = event.getEntityPersister(); return ( (ReactiveEntityPersister) entityPersister) .reactiveLoadEntityIdByNaturalId( event.getOrderedNaturalIdValues(), event.getLockOptions(), session ) .thenApply( pk -> { - if (statisticsEnabled) { + if ( statisticsEnabled ) { long milliseconds = MILLISECONDS.convert( System.nanoTime() - startTime, NANOSECONDS ); statistics.naturalIdQueryExecuted( entityPersister.getRootEntityName(), milliseconds ); } //PK can be null if the entity doesn't exist - if (pk != null) { + if ( pk != null ) { getNaturalIdResolutions( event ) - .cacheResolutionFromLoad( pk, event.getOrderedNaturalIdValues(), event.getEntityPersister() ); + .cacheResolutionFromLoad( pk, event.getOrderedNaturalIdValues(), entityPersister ); } return pk; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java index 7fe85fa69..188e02558 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java @@ -582,7 +582,7 @@ private static void checkMutationQuery(String hqlString, SqmStatement sqmStat public ReactiveMutationQuery createReactiveMutationQuery(CriteriaUpdate updateQuery) { checkOpen(); try { - return new ReactiveQuerySqmImpl( (SqmUpdateStatement) updateQuery, null, this ); + return new ReactiveQuerySqmImpl<>( (SqmUpdateStatement) updateQuery, null, this ); } catch ( RuntimeException e ) { throw getExceptionConverter().convert( e ); @@ -593,7 +593,7 @@ public ReactiveMutationQuery createReactiveMutationQuery(CriteriaUpdate u public ReactiveMutationQuery createReactiveMutationQuery(CriteriaDelete deleteQuery) { checkOpen(); try { - return new ReactiveQuerySqmImpl( (SqmDeleteStatement) deleteQuery, null, this ); + return new ReactiveQuerySqmImpl<>( (SqmDeleteStatement) deleteQuery, null, this ); } catch ( RuntimeException e ) { throw getExceptionConverter().convert( e ); From 949ae57be4fd3568066c2557f5cc3ab649f3958b Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 10 Apr 2023 10:44:49 +0200 Subject: [PATCH 2/3] update ReactiveActions --- .../ReactiveEntityIdentityInsertAction.java | 17 +- .../ReactiveEntityRegularInsertAction.java | 141 +++++------ .../impl/ReactiveEntityUpdateAction.java | 221 +++++++++++------- 3 files changed, 222 insertions(+), 157 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java index 5877870d8..357128db8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java @@ -47,8 +47,7 @@ public void execute() throws HibernateException { @Override public CompletionStage reactiveExecute() throws HibernateException { - - CompletionStage stage = reactiveNullifyTransientReferencesIfNotAlready(); + final CompletionStage stage = reactiveNullifyTransientReferencesIfNotAlready(); final EntityPersister persister = getPersister(); final SharedSessionContractImplementor session = getSession(); @@ -60,12 +59,12 @@ public CompletionStage reactiveExecute() throws HibernateException { // else inserted the same pk first, the insert would fail if ( !isVeto() ) { - ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; + final ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; return stage .thenCompose( v -> reactivePersister.insertReactive( getState(), instance, session ) ) .thenCompose( generatedId -> { setGeneratedId( generatedId ); - return processInsertGenerated( reactivePersister, generatedId, instance, session ) + return processInsertGeneratedProperties( reactivePersister, generatedId, instance, session ) .thenApply( v -> generatedId ); } ) .thenAccept( generatedId -> { @@ -74,7 +73,7 @@ public CompletionStage reactiveExecute() throws HibernateException { persister.setIdentifier( instance, generatedId, session ); final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); persistenceContext.registerInsertedKey( getPersister(), generatedId ); - EntityKey entityKey = session.generateEntityKey( generatedId, persister ); + final EntityKey entityKey = session.generateEntityKey( generatedId, persister ); setEntityKey( entityKey ); persistenceContext.checkUniqueness( entityKey, getInstance() ); @@ -95,13 +94,13 @@ public CompletionStage reactiveExecute() throws HibernateException { } } - private CompletionStage processInsertGenerated( - ReactiveEntityPersister reactivePersister, + private CompletionStage processInsertGeneratedProperties( + ReactiveEntityPersister persister, Object generatedId, Object instance, SharedSessionContractImplementor session) { - return reactivePersister.hasInsertGeneratedProperties() - ? reactivePersister.reactiveProcessInsertGenerated( generatedId, instance, getState(), session ) + return persister.hasInsertGeneratedProperties() + ? persister.reactiveProcessInsertGenerated( generatedId, instance, getState(), session ) : voidFuture(); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java index f95bda991..f113fef89 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java @@ -14,11 +14,14 @@ import org.hibernate.cache.spi.entry.CacheEntry; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; +import org.hibernate.stat.internal.StatsHelper; +import org.hibernate.stat.spi.StatisticsImplementor; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; @@ -50,74 +53,76 @@ public void execute() throws HibernateException { @Override public CompletionStage reactiveExecute() throws HibernateException { + final CompletionStage stage = reactiveNullifyTransientReferencesIfNotAlready(); + + final EntityPersister persister = getPersister(); + final SharedSessionContractImplementor session = getSession(); + final Object instance = getInstance(); + final Object id = getId(); + + // FIXME: It needs to become async + final boolean veto = preInsert(); + + // Don't need to lock the cache here, since if someone + // else inserted the same pk first, the insert would fail + if ( !veto ) { + final ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + return stage + .thenCompose( v -> reactivePersister.insertReactive( id, getState(), instance, session ) ) + .thenApply( res -> { + final EntityEntry entry = persistenceContext.getEntry( instance ); + if ( entry == null ) { + throw new AssertionFailure( "possible non-threadsafe access to session" ); + } + entry.postInsert( getState() ); + return entry; + } ) + .thenCompose( entry -> processInsertGeneratedProperties( reactivePersister, session, instance, id, entry ) ) + .thenAccept( vv -> { + persistenceContext.registerInsertedKey( persister, getId() ); + addCollectionsByKeyToPersistenceContext( persistenceContext, getState() ); + putCacheIfNecessary(); + handleNaturalIdPostSaveNotifications( id ); + postInsert(); + + final StatisticsImplementor statistics = session.getFactory().getStatistics(); + if ( statistics.isStatisticsEnabled() && !veto ) { + statistics.insertEntity( getPersister().getEntityName() ); + } + + markExecuted(); + } ); + } + else { + putCacheIfNecessary(); + handleNaturalIdPostSaveNotifications( id ); + postInsert(); + markExecuted(); + return stage; + } + } - return reactiveNullifyTransientReferencesIfNotAlready().thenCompose( v-> { - - EntityPersister persister = getPersister(); - final SharedSessionContractImplementor session = getSession(); - final Object instance = getInstance(); - final Object id = getId(); - - // FIXME: It needs to become async - final boolean veto = preInsert(); - - // Don't need to lock the cache here, since if someone - // else inserted the same pk first, the insert would fail - CompletionStage insertStage; - if ( !veto ) { - ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; - insertStage = reactivePersister.insertReactive( id, getState(), instance, session ) - .thenApply( res -> { - EntityEntry entry = session.getPersistenceContext().getEntry( instance ); - if ( entry == null ) { - throw new AssertionFailure( "possible non-threadsafe access to session" ); - } - entry.postInsert( getState() ); - return entry; - } ) - .thenCompose( entry -> processInsertGeneratedProperties( reactivePersister, session, instance, id, entry ) - .thenAccept( vv -> session.getPersistenceContext().registerInsertedKey( persister, getId() ) ) ); - } - else { - insertStage = voidFuture(); + //TODO: copy/paste from superclass (make it protected) + private void putCacheIfNecessary() { + final EntityPersister persister = getPersister(); + final SharedSessionContractImplementor session = getSession(); + if ( isCachePutEnabled( persister, session ) ) { + final SessionFactoryImplementor factory = session.getFactory(); + final CacheEntry ce = persister.buildCacheEntry( getInstance(), getState(), getVersion(), session ); + setCacheEntry( persister.getCacheEntryStructure().structure( ce ) ); + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + final Object ck = cache.generateCacheKey( getId(), persister, factory, session.getTenantIdentifier() ); + final boolean put = cacheInsert( persister, ck ); + + final StatisticsImplementor statistics = factory.getStatistics(); + if ( put && statistics.isStatisticsEnabled() ) { + statistics.entityCachePut( + StatsHelper.INSTANCE.getRootEntityRole( persister ), + cache.getRegion().getName() + ); } - - return insertStage.thenApply( res -> { - final SessionFactoryImplementor factory = session.getFactory(); - - if ( isCachePutEnabled( persister, session ) ) { - final CacheEntry ce = persister.buildCacheEntry( - instance, - getState(), - getVersion(), - session - ); - setCacheEntry( persister.getCacheEntryStructure().structure( ce ) ); - final EntityDataAccess cache = persister.getCacheAccessStrategy(); - final Object ck = cache.generateCacheKey( id, persister, factory, session.getTenantIdentifier() ); - - final boolean put = cacheInsert( persister, ck ); - - if ( put && factory.getStatistics().isStatisticsEnabled() ) { - factory.getStatistics().entityCachePut( - persister.getNavigableRole(), - persister.getCacheAccessStrategy().getRegion().getName() - ); - } - } - - handleNaturalIdPostSaveNotifications( id ); - - postInsert(); - - if ( factory.getStatistics().isStatisticsEnabled() && !veto ) { - factory.getStatistics().insertEntity( getEntityName() ); - } - - markExecuted(); - return null; - } ); - } ); + } } private CompletionStage processInsertGeneratedProperties( @@ -135,7 +140,9 @@ private CompletionStage processInsertGeneratedProperties( .thenAccept( v -> entry.postUpdate( instance, getState(), getVersion() ) ); } - return voidFuture(); + else { + return voidFuture(); + } } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java index 103a0f6d2..9ef65faee 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java @@ -8,21 +8,23 @@ import java.util.concurrent.CompletionStage; import org.hibernate.AssertionFailure; +import org.hibernate.CacheMode; import org.hibernate.HibernateException; import org.hibernate.action.internal.EntityUpdateAction; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.entry.CacheEntry; import org.hibernate.engine.spi.CachedNaturalIdValueSource; import org.hibernate.engine.spi.EntityEntry; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.EventSource; +import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.engine.ReactiveExecutable; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; import org.hibernate.stat.internal.StatsHelper; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.tuple.entity.EntityMetamodel; import org.hibernate.type.TypeHelper; import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; @@ -66,41 +68,30 @@ public ReactiveEntityUpdateAction( @Override public CompletionStage reactiveExecute() throws HibernateException { - final Object id = getId(); - final EntityPersister persister = getPersister(); - final SharedSessionContractImplementor session = getSession(); - final Object instance = getInstance(); - if ( preUpdate() ) { return voidFuture(); } - final SessionFactoryImplementor factory = session.getFactory(); - final Object previousVersion = persister.isVersionPropertyGenerated() - // we need to grab the version value from the entity, otherwise - // we have issues with generated-version entities that may have - // multiple actions queued during the same flush - ? persister.getVersion( instance ) - : getPreviousVersion(); - - final Object ck; - if ( persister.canWriteToCache() ) { - final EntityDataAccess cache = persister.getCacheAccessStrategy(); - ck = cache.generateCacheKey( - id, - persister, - factory, - session.getTenantIdentifier() - ); - setLock( cache.lockItem( session, ck, previousVersion ) ); - } - else { - ck = null; - } + final Object id = getId(); + final EntityPersister persister = getPersister(); + final SharedSessionContractImplementor session = getSession(); + final Object instance = getInstance(); + final Object previousVersion = getPreviousVersion(); + final Object ck = lockCacheItem( previousVersion ); - ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; + final ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; return reactivePersister - .updateReactive( id, getState(), getDirtyFields(), hasDirtyCollection(), getPreviousState(), previousVersion, instance, getRowId(), session ) + .updateReactive( + id, + getState(), + getDirtyFields(), + hasDirtyCollection(), + getPreviousState(), + previousVersion, + instance, + getRowId(), + session + ) .thenApply( res -> { final EntityEntry entry = session.getPersistenceContextInternal().getEntry( instance ); if ( entry == null ) { @@ -108,65 +99,131 @@ public CompletionStage reactiveExecute() throws HibernateException { } return entry; } ) - .thenCompose( entry -> { - if ( entry.getStatus() == Status.MANAGED || persister.isVersionPropertyGenerated() ) { - // get the updated snapshot of the entity state by cloning current state; - // it is safe to copy in place, since by this time no-one else (should have) - // has a reference to the array - TypeHelper.deepCopy( - getState(), - persister.getPropertyTypes(), - persister.getPropertyCheckability(), - getState(), - session - ); - return processGeneratedProperties( id, reactivePersister, session, instance ) - // have the entity entry doAfterTransactionCompletion post-update processing, passing it the - // update state and the new version (if one). - .thenAccept( v -> entry.postUpdate( instance, getState(), getNextVersion() ) ) - .thenApply( v -> entry ); - } - return completedFuture( entry ); - } ) + .thenCompose( this::handleGeneratedProperties ) .thenAccept( entry -> { - final StatisticsImplementor statistics = factory.getStatistics(); - if ( persister.canWriteToCache() ) { - if ( persister.isCacheInvalidationRequired() || entry.getStatus() != Status.MANAGED ) { - persister.getCacheAccessStrategy().remove( session, ck ); - } - else if ( session.getCacheMode().isPutEnabled() ) { - //TODO: inefficient if that cache is just going to ignore the updated state! - final CacheEntry ce = persister.buildCacheEntry( instance, getState(), getNextVersion(), getSession() ); - setCacheEntry( persister.getCacheEntryStructure().structure( ce ) ); - - final boolean put = updateCache( persister, previousVersion, ck ); - if ( put && statistics.isStatisticsEnabled() ) { - statistics.entityCachePut( - StatsHelper.INSTANCE.getRootEntityRole( persister ), - getPersister().getCacheAccessStrategy().getRegion().getName() - ); - } - } - } - - if ( getNaturalIdMapping() != null ) { - session.getPersistenceContextInternal().getNaturalIdResolutions().manageSharedResolution( - id, - getNaturalIdMapping().extractNaturalIdFromEntityState( getState() ), - getPreviousNaturalIdValues(), - persister, - CachedNaturalIdValueSource.UPDATE - ); - } - + handleDeleted( entry, persister, instance ); + updateCacheItem( persister, ck, entry ); + handleNaturalIdResolutions( persister, session, id ); postUpdate(); + final StatisticsImplementor statistics = session.getFactory().getStatistics(); if ( statistics.isStatisticsEnabled() ) { statistics.updateEntity( getPersister().getEntityName() ); } } ); } + private CompletionStage handleGeneratedProperties(EntityEntry entry) { + final EntityPersister persister = getPersister(); + if ( entry.getStatus() == Status.MANAGED || persister.isVersionPropertyGenerated() ) { + final SharedSessionContractImplementor session = getSession(); + final Object instance = getInstance(); + final Object id = getId(); + // get the updated snapshot of the entity state by cloning current state; + // it is safe to copy in place, since by this time no-one else (should have) + // has a reference to the array + TypeHelper.deepCopy( + getState(), + persister.getPropertyTypes(), + persister.getPropertyCheckability(), + getState(), + session + ); + final ReactiveEntityPersister reactivePersister = (ReactiveEntityPersister) persister; + return processGeneratedProperties( id, reactivePersister, session, instance ) + // have the entity entry doAfterTransactionCompletion post-update processing, passing it the + // update state and the new version (if one). + .thenAccept( v -> entry.postUpdate( instance, getState(), getNextVersion() ) ) + .thenApply( v -> entry ); + } + else { + return completedFuture( entry ); + } + } + + // TODO: copy/paste from superclass (make it protected) + private void handleDeleted(EntityEntry entry, EntityPersister persister, Object instance) { + if ( entry.getStatus() == Status.DELETED ) { + final EntityMetamodel entityMetamodel = persister.getEntityMetamodel(); + final boolean isImpliedOptimisticLocking = !entityMetamodel.isVersioned() + && entityMetamodel.getOptimisticLockStyle().isAllOrDirty(); + if ( isImpliedOptimisticLocking && entry.getLoadedState() != null ) { + // The entity will be deleted and because we are going to create a delete statement + // that uses all the state values in the where clause, the entry state needs to be + // updated otherwise the statement execution will not delete any row (see HHH-15218). + entry.postUpdate(instance, getState(), getNextVersion() ); + } + } + } + + // TODO: copy/paste from superclass (make it protected) + private void handleNaturalIdResolutions(EntityPersister persister, SharedSessionContractImplementor session, Object id) { + NaturalIdMapping naturalIdMapping = getNaturalIdMapping(); + if ( naturalIdMapping != null ) { + session.getPersistenceContextInternal().getNaturalIdResolutions().manageSharedResolution( + id, + naturalIdMapping.extractNaturalIdFromEntityState( getState() ), + getPreviousNaturalIdValues(), + persister, + CachedNaturalIdValueSource.UPDATE + ); + } + } + + // TODO: copy/paste from superclass (make it protected) + private void updateCacheItem(Object previousVersion, Object ck, EntityEntry entry) { + final EntityPersister persister = getPersister(); + if ( persister.canWriteToCache() ) { + final SharedSessionContractImplementor session = getSession(); + if ( isCacheInvalidationRequired( persister, session ) || entry.getStatus() != Status.MANAGED ) { + persister.getCacheAccessStrategy().remove( session, ck ); + } + else if ( session.getCacheMode().isPutEnabled() ) { + //TODO: inefficient if that cache is just going to ignore the updated state! + final CacheEntry ce = persister.buildCacheEntry( getInstance(), getState(), getNextVersion(), getSession() ); + setCacheEntry( persister.getCacheEntryStructure().structure( ce ) ); + final boolean put = updateCache( persister, previousVersion, ck ); + + final StatisticsImplementor statistics = session.getFactory().getStatistics(); + if ( put && statistics.isStatisticsEnabled() ) { + statistics.entityCachePut( + StatsHelper.INSTANCE.getRootEntityRole(persister), + getPersister().getCacheAccessStrategy().getRegion().getName() + ); + } + } + } + } + + private static boolean isCacheInvalidationRequired( + EntityPersister persister, + SharedSessionContractImplementor session) { + // the cache has to be invalidated when CacheMode is equal to GET or IGNORE + return persister.isCacheInvalidationRequired() + || session.getCacheMode() == CacheMode.GET + || session.getCacheMode() == CacheMode.IGNORE; + } + + // TODO: copy/paste from superclass (make it protected) + private Object lockCacheItem(Object previousVersion) { + final EntityPersister persister = getPersister(); + if ( persister.canWriteToCache() ) { + final SharedSessionContractImplementor session = getSession(); + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + final Object ck = cache.generateCacheKey( + getId(), + persister, + session.getFactory(), + session.getTenantIdentifier() + ); + setLock( cache.lockItem( session, ck, previousVersion ) ); + return ck; + } + else { + return null; + } + } + private CompletionStage processGeneratedProperties( Object id, ReactiveEntityPersister persister, @@ -182,7 +239,9 @@ private CompletionStage processGeneratedProperties( return persister.reactiveProcessUpdateGenerated( id, instance, getState(), session ); } - return voidFuture(); + else { + return voidFuture(); + } } @Override From 299027f2af66a6ae85ac1ebc9a302f5297639bdf Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 10 Apr 2023 11:40:25 +0200 Subject: [PATCH 3/3] update DefaultReactiveLoadEventListener --- .../DefaultReactiveLoadEventListener.java | 223 +++++++++++------- .../org/hibernate/reactive/mutiny/Mutiny.java | 13 +- .../org/hibernate/reactive/stage/Stage.java | 11 +- 3 files changed, 156 insertions(+), 91 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java index 053c395af..3a9da13d4 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java @@ -15,18 +15,20 @@ import org.hibernate.PersistentObjectException; import org.hibernate.TypeMismatchException; import org.hibernate.action.internal.DelayedPostInsertIdentifier; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.SoftLock; -import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.PersistentAttributeInterceptable; +import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.engine.spi.Status; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; -import org.hibernate.event.spi.LoadEventListener;import org.hibernate.event.spi.LoadEventListener.LoadType; +import org.hibernate.event.spi.LoadEventListener; import org.hibernate.loader.ast.internal.CacheEntityLoaderHelper; +import org.hibernate.loader.ast.internal.CacheEntityLoaderHelper.PersistenceContextEntry; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMappingsList; import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; @@ -42,8 +44,11 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; +import org.hibernate.reactive.session.impl.ReactiveQueryExecutorLookup; import org.hibernate.stat.spi.StatisticsImplementor; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.pretty.MessageHelper.infoString; import static org.hibernate.reactive.session.impl.SessionUtil.checkEntityFound; import static org.hibernate.reactive.session.impl.SessionUtil.throwEntityNotFound; @@ -91,7 +96,7 @@ public void onLoad(LoadEvent event, LoadType loadType) throws HibernateException // Since this method is not reactive, we're not expecting to hit the // database here (if we do, it's a bug) and so we can assume the // returned CompletionStage is already completed - CompletionStage checkId = checkId( event, loadType, persister ); + final CompletionStage checkId = checkId( event, loadType, persister ); if ( !checkId.toCompletableFuture().isDone() ) { // This only happens if the object is loaded from the db throw new UnexpectedAccessToTheDatabase(); @@ -101,7 +106,7 @@ public void onLoad(LoadEvent event, LoadType loadType) throws HibernateException // Since this method is not reactive, we're not expecting to hit the // database here (if we do, it's a bug) and so we can assume the // returned CompletionStage is already completed (a proxy, perhaps) - CompletionStage loaded = doOnLoad( persister, event, loadType ); + final CompletionStage loaded = doOnLoad( persister, event, loadType ); if ( !loaded.toCompletableFuture().isDone() ) { // This only happens if the object is loaded from the db throw new UnexpectedAccessToTheDatabase(); @@ -244,7 +249,8 @@ else if ( idMapping instanceof NonAggregatedIdentifierMapping ) { } } } - throw new TypeMismatchException( "Provided id of the wrong type for class " + persister.getEntityName() + ". Expected: " + idClass + ", got " + event.getEntityId().getClass() ); + throw new TypeMismatchException( "Provided id of the wrong type for class " + persister.getEntityName() + + ". Expected: " + idClass + ", got " + event.getEntityId().getClass() ); } /* @@ -338,8 +344,7 @@ else if ( persister.hasProxy() ) { } private static boolean wasDeleted(PersistenceContext persistenceContext, Object existing) { - final Status status = persistenceContext.getEntry( existing ).getStatus(); - return status == Status.DELETED || status == Status.GONE; + return persistenceContext.getEntry( existing ).getStatus().isDeletedOrGone(); } private CompletionStage loadWithBytecodeProxy(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { @@ -372,12 +377,12 @@ private CompletionStage loadWithRegularProxy(LoadEvent event, EntityPers final Object proxy = event.getSession().getPersistenceContextInternal().getProxy( keyToLoad ); if ( proxy != null ) { // narrow the existing proxy to the type we're looking for - return returnNarrowedProxy( event, persister, keyToLoad, options, proxy ); + return narrowedProxy( event, persister, keyToLoad, options, proxy ); } else if ( options.isAllowProxyCreation() ) { // return a new proxy // ORM calls DefaultLoadEventListener#proxyOrCache - return completedFuture( createProxyIfNecessary( event, persister, keyToLoad, options ) ); + return completedFuture( proxyOrCached( event, persister, keyToLoad, options ) ); } else { return load( event, persister, keyToLoad, options ); @@ -404,9 +409,7 @@ private static CompletionStage loadWithProxyFactory(LoadEvent event, Ent } else if ( persister.hasSubclasses() ) { // specialized handling for entities with subclasses with a HibernateProxy factory - // entities with subclasses that define a ProxyFactory can create a HibernateProxy - // Maybe we can get it from the cache: Check DefaultLoadEventListener#proxyOrCache - return completedFuture( createProxy( event, persister, keyToLoad ) ); + return completedFuture( proxyOrCached( event, persister, keyToLoad ) ); } else { // no existing proxy, and no subclasses @@ -427,22 +430,40 @@ private static PersistentAttributeInterceptable createBatchLoadableEnhancedProxy return persister.getBytecodeEnhancementMetadata().createEnhancedProxy( keyToLoad, true, session ); } - private CompletionStage proxyImplementation(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { - return load( event, persister, keyToLoad, options ) - .thenApply( optional -> { - if (optional != null ) { - return optional; - } - if ( options != LoadEventListener.INTERNAL_LOAD_NULLABLE ) { - // throw an appropriate exception - event.getSession().getFactory().getEntityNotFoundDelegate() - .handleEntityNotFound( persister.getEntityName(), keyToLoad.getIdentifier() ); - } - // Otherwise, if it's INTERNAL_LOAD_NULLABLE, the proxy is - // for a non-existing association mapped as @NotFound. - // Don't throw an exception; just return null. - return null; - } ); + private static Object proxyOrCached(LoadEvent event, EntityPersister persister, EntityKey keyToLoad) { + final Object cachedEntity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache( + event.getSession(), + null, + LockMode.NONE, + persister, + keyToLoad + ); + if ( cachedEntity != null ) { + return cachedEntity; + } + // entities with subclasses that define a ProxyFactory can create a HibernateProxy + return createProxy( event, persister, keyToLoad ); + } + + private static Object proxyOrCached(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { + final PersistenceContext persistenceContext = event.getSession().getPersistenceContext(); + final Object existing = persistenceContext.getEntity( keyToLoad ); + if ( existing != null ) { + return options.isCheckDeleted() && wasDeleted( persistenceContext, existing ) ? null : existing; + } + if ( persister.hasSubclasses() ) { + final Object cachedEntity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache( + event.getSession(), + null, + LockMode.NONE, + persister, + keyToLoad + ); + if ( cachedEntity != null ) { + return cachedEntity; + } + } + return createProxyIfNecessary( event, persister, keyToLoad, options ); } /** @@ -457,7 +478,7 @@ private CompletionStage proxyImplementation(LoadEvent event, EntityPersi * * @return The created/existing proxy */ - private CompletionStage returnNarrowedProxy( + private CompletionStage narrowedProxy( LoadEvent event, EntityPersister persister, EntityKey keyToLoad, @@ -468,23 +489,43 @@ private CompletionStage returnNarrowedProxy( } LazyInitializer li = ( (HibernateProxy) proxy ).getHibernateLazyInitializer(); - if ( li.isUnwrap() ) { return completedFuture( li.getImplementation() ); } - - final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal(); - if ( options.isAllowProxyCreation() ) { - return completedFuture( persistenceContext.narrowProxy( proxy, persister, keyToLoad, null ) ); - } else { - return proxyImplementation( event, persister, keyToLoad, options ) - .thenApply( impl -> impl == null - ? null - : persistenceContext.narrowProxy( proxy, persister, keyToLoad, impl ) ); + final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal(); + if ( options.isAllowProxyCreation() ) { + return completedFuture( persistenceContext.narrowProxy( proxy, persister, keyToLoad, null ) ); + } + else { + return proxyImplementation( event, persister, keyToLoad, options ) + .thenApply( impl -> impl == null + ? null + : persistenceContext.narrowProxy( proxy, persister, keyToLoad, impl ) ); + } } } + private CompletionStage proxyImplementation(LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { + return load( event, persister, keyToLoad, options ) + .thenApply( optional -> { + if ( optional != null ) { + return optional; + } + else { + if ( options != LoadEventListener.INTERNAL_LOAD_NULLABLE ) { + // throw an appropriate exception + event.getSession().getFactory().getEntityNotFoundDelegate() + .handleEntityNotFound( persister.getEntityName(), keyToLoad.getIdentifier() ); + } + // Otherwise, if it's INTERNAL_LOAD_NULLABLE, the proxy is + // for a non-existing association mapped as @NotFound. + // Don't throw an exception; just return null. + return null; + } + } ); + } + /** * If there is already a corresponding proxy associated with the * persistence context, return it; otherwise create a proxy, associate it @@ -497,38 +538,29 @@ private CompletionStage returnNarrowedProxy( * * @return The created/existing proxy */ - private Object createProxyIfNecessary( + private static Object createProxyIfNecessary( LoadEvent event, EntityPersister persister, EntityKey keyToLoad, LoadType options) { final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal(); final Object existing = persistenceContext.getEntity( keyToLoad ); - final boolean traceEnabled = LOG.isTraceEnabled(); if ( existing != null ) { // return existing object or initialized proxy (unless deleted) - if ( traceEnabled ) { + if ( LOG.isTraceEnabled() ) { LOG.trace( "Entity found in session cache" ); } - if ( options.isCheckDeleted() ) { - EntityEntry entry = persistenceContext.getEntry( existing ); - Status status = entry.getStatus(); - if ( status == Status.DELETED || status == Status.GONE ) { - return null; - } - } - return existing; + return options.isCheckDeleted() && wasDeleted( persistenceContext, existing ) ? null : existing; } - if ( traceEnabled ) { - LOG.trace( "Creating new proxy for entity" ); + else { + if ( LOG.isTraceEnabled() ) { + LOG.trace( "Creating new proxy for entity" ); + } + return createProxy( event, persister, keyToLoad ); } - return createProxy( event, persister, keyToLoad ); } - private static Object createProxy( - LoadEvent event, - EntityPersister persister, - EntityKey keyToLoad) { + private static Object createProxy(LoadEvent event, EntityPersister persister, EntityKey keyToLoad) { // return new uninitialized proxy Object proxy = persister.createProxy( event.getEntityId(), event.getSession() ); PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal(); @@ -611,28 +643,59 @@ private CompletionStage doLoad( EntityPersister persister, EntityKey keyToLoad, LoadType options) { - - final EventSource session = event.getSession(); - final boolean traceEnabled = LOG.isTraceEnabled(); - if ( traceEnabled ) { + if ( LOG.isTraceEnabled() ) { LOG.tracev( "Attempting to resolve: {0}", - infoString( persister, event.getEntityId(), session.getFactory() ) + infoString( persister, event.getEntityId(), event.getSession().getFactory() ) ); } - final CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = - ReactiveCacheEntityLoaderHelper.INSTANCE.loadFromSessionCache( event, keyToLoad, options ); - Object entity = persistenceContextEntry.getEntity(); - if ( entity != null ) { - return persistenceContextEntry.isManaged() - ? completedFuture( entity ) - : nullFuture(); + if ( event.getSession().getPersistenceContextInternal().containsDeletedUnloadedEntityKey( keyToLoad ) ) { + return nullFuture(); + } + else { + final PersistenceContextEntry persistenceContextEntry = + ReactiveCacheEntityLoaderHelper.INSTANCE.loadFromSessionCache( event, keyToLoad, options ); + final Object entity = persistenceContextEntry.getEntity(); + if ( entity != null ) { + return persistenceContextEntry.isManaged() ? initializeIfNecessary( entity ) : nullFuture(); + } + else { + return loadFromCacheOrDatasource( event, persister, keyToLoad ); + } } + } - entity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache( event, persister, keyToLoad ); + private static CompletionStage initializeIfNecessary(Object entity) { + if ( isPersistentAttributeInterceptable( entity ) ) { + final PersistentAttributeInterceptable interceptable = asPersistentAttributeInterceptable( entity ); + final PersistentAttributeInterceptor interceptor = interceptable.$$_hibernate_getInterceptor(); + if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor) { + final EnhancementAsProxyLazinessInterceptor lazinessInterceptor = + (EnhancementAsProxyLazinessInterceptor) interceptor; + final SharedSessionContractImplementor session = lazinessInterceptor.getLinkedSession(); + if ( session == null ) { + throw LOG.sessionClosedLazyInitializationException(); + } + return ReactiveQueryExecutorLookup.extract( session ).reactiveFetch( entity, false ); + } + else { + return completedFuture( entity ); + } + } + else { + return completedFuture( entity ); + } + } + + private CompletionStage loadFromCacheOrDatasource( + LoadEvent event, + EntityPersister persister, + EntityKey keyToLoad) { + final EventSource session = event.getSession(); + final Object entity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache(event, persister, keyToLoad); if ( entity != null ) { - if ( traceEnabled ) { + if ( LOG.isTraceEnabled() ) { LOG.tracev( "Resolved object in second-level cache: {0}", infoString( persister, event.getEntityId(), session.getFactory() ) @@ -642,7 +705,7 @@ private CompletionStage doLoad( return completedFuture( entity ); } else { - if ( traceEnabled ) { + if ( LOG.isTraceEnabled() ) { LOG.tracev( "Object not resolved in any cache: {0}", infoString( persister, event.getEntityId(), session.getFactory() ) @@ -650,7 +713,7 @@ private CompletionStage doLoad( } return loadFromDatasource( event, persister ) .thenApply( optional -> { - if ( optional!=null ) { + if ( optional != null ) { cacheNaturalId( event, persister, session, optional ); } return optional; @@ -661,9 +724,10 @@ private CompletionStage doLoad( private void cacheNaturalId(LoadEvent event, EntityPersister persister, EventSource session, Object entity) { if ( entity != null && persister.hasNaturalIdentifier() ) { session.getPersistenceContextInternal().getNaturalIdResolutions() - .cacheResolutionFromLoad( event.getEntityId(), - persister.getNaturalIdMapping().extractNaturalIdFromEntity( entity ), - persister + .cacheResolutionFromLoad( + event.getEntityId(), + persister.getNaturalIdMapping().extractNaturalIdFromEntity( entity ), + persister ); } } @@ -692,8 +756,9 @@ protected CompletionStage loadFromDatasource(LoadEvent event, EntityPers // persister/loader/initializer sensitive to this fact - possibly // passing LoadType along - if ( entity instanceof HibernateProxy ) { - entity = ( (HibernateProxy) entity ).getHibernateLazyInitializer().getImplementation(); + final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); + if ( lazyInitializer != null ) { + entity = lazyInitializer.getImplementation(); } final StatisticsImplementor statistics = event.getSession().getFactory().getStatistics(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java index 181efe077..a7476a048 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java @@ -28,7 +28,6 @@ import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.collection.spi.AbstractPersistentCollection; import org.hibernate.collection.spi.PersistentCollection; -import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -39,7 +38,6 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.query.BindableType; import org.hibernate.query.CommonQueryContract; -import org.hibernate.query.NativeQuery; import org.hibernate.query.ParameterMetadata; import org.hibernate.query.QueryParameter; import org.hibernate.query.ResultListTransformer; @@ -70,6 +68,8 @@ import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.SingularAttribute; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.internal.util.LockModeConverter.convertToLockMode; import static org.hibernate.jpa.internal.util.CacheModeHelper.interpretCacheMode; import static org.hibernate.jpa.internal.util.CacheModeHelper.interpretCacheRetrieveMode; @@ -2751,17 +2751,16 @@ static Uni fetch(T association) { return Uni.createFrom().nullItem(); } - SharedSessionContractImplementor session; + final SharedSessionContractImplementor session; if ( association instanceof HibernateProxy ) { session = ( (HibernateProxy) association ).getHibernateLazyInitializer().getSession(); } else if ( association instanceof PersistentCollection ) { //this unfortunately doesn't work for stateless session because the session ref gets set to null - session = ( (AbstractPersistentCollection) association ).getSession(); + session = ( (AbstractPersistentCollection) association ).getSession(); } - else if ( ManagedTypeHelper.isPersistentAttributeInterceptable( association ) ) { - final PersistentAttributeInterceptable interceptable = ManagedTypeHelper.asPersistentAttributeInterceptable( - association ); + else if ( isPersistentAttributeInterceptable( association ) ) { + final PersistentAttributeInterceptable interceptable = asPersistentAttributeInterceptable( association ); final PersistentAttributeInterceptor interceptor = interceptable.$$_hibernate_getInterceptor(); if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) { session = ( (EnhancementAsProxyLazinessInterceptor) interceptor ).getLinkedSession(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java index f951b9f31..1b71e7d3f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java @@ -27,7 +27,6 @@ import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.collection.spi.AbstractPersistentCollection; import org.hibernate.collection.spi.PersistentCollection; -import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -69,6 +68,8 @@ import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.SingularAttribute; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.internal.util.LockModeConverter.convertToLockMode; import static org.hibernate.jpa.internal.util.CacheModeHelper.interpretCacheMode; import static org.hibernate.jpa.internal.util.CacheModeHelper.interpretCacheRetrieveMode; @@ -2801,15 +2802,15 @@ static CompletionStage fetch(T association) { return CompletionStages.nullFuture(); } - SharedSessionContractImplementor session; + final SharedSessionContractImplementor session; if ( association instanceof HibernateProxy) { session = ( (HibernateProxy) association ).getHibernateLazyInitializer().getSession(); } else if ( association instanceof PersistentCollection) { - session = ( (AbstractPersistentCollection) association ).getSession(); + session = ( (AbstractPersistentCollection) association ).getSession(); } - else if ( ManagedTypeHelper.isPersistentAttributeInterceptable( association ) ) { - final PersistentAttributeInterceptable interceptable = ManagedTypeHelper.asPersistentAttributeInterceptable( association ); + else if ( isPersistentAttributeInterceptable( association ) ) { + final PersistentAttributeInterceptable interceptable = asPersistentAttributeInterceptable( association ); final PersistentAttributeInterceptor interceptor = interceptable.$$_hibernate_getInterceptor(); if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor) { session = ( (EnhancementAsProxyLazinessInterceptor) interceptor ).getLinkedSession();