/* Hibernate, Relational Persistence for Idiomatic Java * * SPDX-License-Identifier: LGPL-2.1-or-later * Copyright: Red Hat Inc. and Hibernate Authors */ package org.hibernate.reactive.event.impl; import org.hibernate.*; import org.hibernate.cache.spi.access.CollectionDataAccess; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.SoftLock; import org.hibernate.engine.internal.CascadePoint; import org.hibernate.engine.spi.*; import org.hibernate.event.internal.EvictVisitor; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.RefreshEvent; import org.hibernate.event.spi.RefreshEventListener; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.collections.IdentitySet; import org.hibernate.metamodel.spi.MetamodelImplementor; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; import org.hibernate.reactive.engine.impl.Cascade; import org.hibernate.reactive.engine.impl.CascadingActions; import org.hibernate.reactive.event.ReactiveRefreshEventListener; import org.hibernate.reactive.persister.entity.impl.ReactiveAbstractEntityPersister; import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import java.io.Serializable; import java.util.Map; import java.util.concurrent.CompletionStage; /** * A reactific {@link org.hibernate.event.internal.DefaultRefreshEventListener}. */ public class DefaultReactiveRefreshEventListener implements RefreshEventListener, ReactiveRefreshEventListener { private static final CoreMessageLogger LOG = CoreLogging.messageLogger( DefaultReactiveRefreshEventListener.class ); public CompletionStage<Void> reactiveOnRefresh(RefreshEvent event) throws HibernateException { return reactiveOnRefresh( event, new IdentitySet( 10 ) ); } @Override public void onRefresh(RefreshEvent event) throws HibernateException { throw new UnsupportedOperationException(); } @Override public void onRefresh(RefreshEvent event, Map refreshedAlready) throws HibernateException { throw new UnsupportedOperationException(); } /** * Handle the given refresh event. * * @param event The refresh event to be handled. */ public CompletionStage<Void> reactiveOnRefresh(RefreshEvent event, IdentitySet refreshedAlready) { final EventSource source = event.getSession(); boolean isTransient; if ( event.getEntityName() != null ) { isTransient = !source.contains( event.getEntityName(), event.getObject() ); } else { isTransient = !source.contains( event.getObject() ); } final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); if ( persistenceContext.reassociateIfUninitializedProxy( event.getObject() ) ) { if ( isTransient ) { source.setReadOnly( event.getObject(), source.isDefaultReadOnly() ); } return CompletionStages.nullFuture(); } final Object object = persistenceContext.unproxyAndReassociate( event.getObject() ); if ( refreshedAlready.contains( object ) ) { LOG.trace( "Already refreshed" ); return CompletionStages.nullFuture(); } final EntityEntry e = persistenceContext.getEntry( object ); final EntityPersister persister; final Serializable id; if ( e == null ) { persister = source.getEntityPersister( event.getEntityName(), object ); //refresh() does not pass an entityName id = persister.getIdentifier( object, event.getSession() ); if ( LOG.isTraceEnabled() ) { LOG.tracev( "Refreshing transient {0}", MessageHelper.infoString( persister, id, source.getFactory() ) ); } final EntityKey key = source.generateEntityKey( id, persister ); if ( persistenceContext.getEntry( key ) != null ) { throw new PersistentObjectException( "attempted to refresh transient instance when persistent instance was already associated with the Session: " + MessageHelper.infoString( persister, id, source.getFactory() ) ); } } else { if ( LOG.isTraceEnabled() ) { LOG.tracev( "Refreshing ", MessageHelper.infoString( e.getPersister(), e.getId(), source.getFactory() ) ); } if ( !e.isExistsInDatabase() ) { throw new UnresolvableObjectException( e.getId(), "this instance does not yet exist as a row in the database" ); } persister = e.getPersister(); id = e.getId(); } // cascade the refresh prior to refreshing this entity refreshedAlready.add( object ); return cascadeRefresh( source, persister, object, refreshedAlready ) .thenCompose(v -> { if ( e != null ) { final EntityKey key = source.generateEntityKey( id, persister ); persistenceContext.removeEntity( key ); if ( persister.hasCollections() ) { new EvictVisitor( source, object ).process( object, 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( object ); } 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 ) ); } evictCachedCollections( persister, id, source ); String previousFetchProfile = source.getLoadQueryInfluencers().getInternalFetchProfile(); source.getLoadQueryInfluencers().setInternalFetchProfile( "refresh" ); // 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 ( e != null ) { final LockMode currentLockMode = e.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 ) { // our transaction should already hold the exclusive lock on // the underlying row - so READ should be sufficient. // // in fact, this really holds true for any current lock-mode that indicates we // hold an exclusive lock on the underlying row - but we *need* to handle // 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; } else { lockOptionsToUse.setLockMode( currentLockMode ); postRefreshLockMode = null; } } else { postRefreshLockMode = null; } } else { postRefreshLockMode = null; } return ( (ReactiveAbstractEntityPersister) persister ).reactiveLoad( id, object, lockOptionsToUse, source ) .thenAccept(result -> { 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 // - however, the refresh operation actually creates a new entry, so get it persistenceContext.getEntry(result).setLockMode(postRefreshLockMode); } // Keep the same read-only/modifiable setting for the entity that it had before refreshing; // If it was transient, then set it to the default for the source. if (!persister.isMutable()) { // this is probably redundant; it should already be read-only source.setReadOnly(result, true); } else { source.setReadOnly(result, e == null ? source.isDefaultReadOnly() : e.isReadOnly()); } } UnresolvableObjectException.throwIfNull(result, id, persister.getEntityName()); }) .whenComplete( (vv,t) -> source.getLoadQueryInfluencers().setInternalFetchProfile(previousFetchProfile) ); } ); } private CompletionStage<Void> cascadeRefresh( EventSource source, EntityPersister persister, Object object, IdentitySet refreshedAlready) { return new Cascade<>( CascadingActions.REFRESH, CascadePoint.BEFORE_REFRESH, persister, object, refreshedAlready, source ).cascade(); } private void evictCachedCollections(EntityPersister persister, Serializable id, EventSource source) { evictCachedCollections( persister.getPropertyTypes(), id, source ); } private void evictCachedCollections(Type[] types, Serializable id, EventSource source) throws HibernateException { final ActionQueue actionQueue = source.getActionQueue(); final SessionFactoryImplementor factory = source.getFactory(); final MetamodelImplementor metamodel = factory.getMetamodel(); for ( Type type : types ) { if ( type.isCollectionType() ) { CollectionPersister collectionPersister = metamodel.collectionPersister( ( (CollectionType) type ).getRole() ); if ( collectionPersister.hasCache() ) { final CollectionDataAccess cache = collectionPersister.getCacheAccessStrategy(); final Object ck = cache.generateCacheKey( id, collectionPersister, factory, source.getTenantIdentifier() ); final SoftLock lock = cache.lockItem( source, ck, null ); cache.remove( source, ck ); actionQueue.registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); } } else if ( type.isComponentType() ) { CompositeType actype = (CompositeType) type; evictCachedCollections( actype.getSubtypes(), id, source ); } } } }