diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 669e0b16ae34..898312adc553 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -382,8 +382,8 @@ public interface Session extends SharedSessionContract, EntityManager { * the database? In other words, would any DML operations be executed if * we flushed this session? * - * @return {@code true} if the session contains pending changes; {@code false} otherwise. - * @throws HibernateException could not perform dirtying checking + * @return {@code true} if the session contains pending changes; + * {@code false} otherwise. */ boolean isDirty(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index 865e08ece267..12d68efaabda 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -100,6 +100,7 @@ import org.hibernate.models.internal.ClassTypeDetailsImpl; import org.hibernate.models.spi.AnnotationTarget; import org.hibernate.models.spi.ClassDetails; +import org.hibernate.models.spi.ClassDetailsRegistry; import org.hibernate.models.spi.MemberDetails; import org.hibernate.models.spi.SourceModelBuildingContext; import org.hibernate.models.spi.TypeDetails; @@ -480,22 +481,9 @@ private boolean mapAsIdClass( // properties and create an identifier mapper containing the id properties of the main entity final ClassDetails classWithIdClass = inheritanceState.getClassWithIdClass( false ); if ( classWithIdClass != null ) { - final IdClass idClassAnn = classWithIdClass.getDirectAnnotationUsage( IdClass.class ); - final ClassDetails compositeClass; - if ( idClassAnn == null ) { - try { - compositeClass = getMetadataCollector().getSourceModelBuildingContext() - .getClassDetailsRegistry() - .resolveClassDetails( inheritanceState.getClassDetails().getClassName() + "_$Id" ); - } - catch (RuntimeException e) { - return false; - } - } - else { - final Class idClassValue = idClassAnn.value(); - compositeClass = getMetadataCollector().getSourceModelBuildingContext() - .getClassDetailsRegistry().resolveClassDetails( idClassValue.getName() ); + final ClassDetails compositeClass = idClassDetails( inheritanceState, classWithIdClass ); + if ( compositeClass == null ) { + return false; } final TypeDetails compositeType = new ClassTypeDetailsImpl( compositeClass, TypeDetails.Kind.CLASS ); final TypeDetails classWithIdType = new ClassTypeDetailsImpl( classWithIdClass, TypeDetails.Kind.CLASS ); @@ -557,6 +545,24 @@ private boolean mapAsIdClass( } } + private ClassDetails idClassDetails(InheritanceState inheritanceState, ClassDetails classWithIdClass) { + final IdClass idClassAnn = classWithIdClass.getDirectAnnotationUsage( IdClass.class ); + final ClassDetailsRegistry classDetailsRegistry = getSourceModelContext().getClassDetailsRegistry(); + if ( idClassAnn == null ) { + try { + // look for an Id class generated by Hibernate Processor as an inner class of static metamodel + final String generatedIdClassName = inheritanceState.getClassDetails().getClassName() + "_$Id"; + return classDetailsRegistry.resolveClassDetails( generatedIdClassName ); + } + catch (RuntimeException e) { + return null; + } + } + else { + return classDetailsRegistry.resolveClassDetails( idClassAnn.value().getName() ); + } + } + private Component createMapperProperty( Map inheritanceStates, PersistentClass persistentClass, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractEntityEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractEntityEntry.java index 8530fb193aec..2d8c38649271 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractEntityEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractEntityEntry.java @@ -357,49 +357,54 @@ public boolean requiresDirtyCheck(Object entity) { } private boolean isUnequivocallyNonDirty(Object entity) { - if ( isSelfDirtinessTracker( entity ) ) { - final boolean uninitializedProxy; - if ( isPersistentAttributeInterceptable( entity ) ) { - final PersistentAttributeInterceptor interceptor = - asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) { - EnhancementAsProxyLazinessInterceptor enhancementAsProxyLazinessInterceptor = - (EnhancementAsProxyLazinessInterceptor) interceptor; - return !enhancementAsProxyLazinessInterceptor.hasWrittenFieldNames(); //EARLY EXIT! - } - else { - uninitializedProxy = false; - } + return isSelfDirtinessTracker( entity ) + ? isNonDirtyViaTracker( entity ) + : isNonDirtyViaCustomStrategy( entity ); + } + + private boolean isNonDirtyViaCustomStrategy(Object entity) { + if ( isPersistentAttributeInterceptable( entity ) ) { + final PersistentAttributeInterceptor interceptor = + asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) { + // we never have to check an uninitialized proxy + // TODO: why do we not check !lazinessInterceptor.hasWrittenFieldNames() + // as we do below in isNonDirtyViaTracker() ? + return true; } - else if ( isHibernateProxy( entity ) ) { - uninitializedProxy = extractLazyInitializer( entity ).isUninitialized(); + } + + final SessionImplementor session = getPersistenceContext().getSession().asSessionImplementor(); + final CustomEntityDirtinessStrategy customEntityDirtinessStrategy = + session.getFactory().getCustomEntityDirtinessStrategy(); + return customEntityDirtinessStrategy.canDirtyCheck( entity, getPersister(), session ) + && !customEntityDirtinessStrategy.isDirty( entity, getPersister(), session ); + } + + private boolean isNonDirtyViaTracker(Object entity) { + final boolean uninitializedProxy; + if ( isPersistentAttributeInterceptable( entity ) ) { + final PersistentAttributeInterceptor interceptor = + asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { + return !lazinessInterceptor.hasWrittenFieldNames(); } else { uninitializedProxy = false; } - // we never have to check an uninitialized proxy - return uninitializedProxy - || !persister.hasCollections() - && !persister.hasMutableProperties() - && !asSelfDirtinessTracker( entity ).$$_hibernate_hasDirtyAttributes() - && asManagedEntity( entity ).$$_hibernate_useTracker(); } - else { - if ( isPersistentAttributeInterceptable( entity ) ) { - final PersistentAttributeInterceptor interceptor = - asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) { - // we never have to check an uninitialized proxy - return true; //EARLY EXIT! - } - } - - final SessionImplementor session = getPersistenceContext().getSession().asSessionImplementor(); - final CustomEntityDirtinessStrategy customEntityDirtinessStrategy = - session.getFactory().getCustomEntityDirtinessStrategy(); - return customEntityDirtinessStrategy.canDirtyCheck( entity, getPersister(), session ) - && !customEntityDirtinessStrategy.isDirty( entity, getPersister(), session ); + else if ( isHibernateProxy( entity ) ) { + uninitializedProxy = extractLazyInitializer( entity ).isUninitialized(); } + else { + uninitializedProxy = false; + } + // we never have to check an uninitialized proxy + return uninitializedProxy + || !persister.hasCollections() + && !persister.hasMutableProperties() + && !asSelfDirtinessTracker( entity ).$$_hibernate_hasDirtyAttributes() + && asManagedEntity( entity ).$$_hibernate_useTracker(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java index 418706053bff..08279d1e37e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java @@ -9,7 +9,6 @@ import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.NullnessUtil; import org.hibernate.persister.collection.CollectionPersister; import java.io.IOException; @@ -20,6 +19,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.pretty.MessageHelper.collectionInfoString; /** @@ -68,11 +68,11 @@ public CollectionEntry(CollectionPersister persister, PersistentCollection co // during flush shouldn't be ignored ignore = false; - collection.clearDirty(); //a newly wrapped collection is NOT dirty (or we get unnecessary version updates) + // a newly wrapped collection is NOT dirty + // (or we get unnecessary version updates) + collection.clearDirty(); - snapshot = persister.isMutable() ? - collection.getSnapshot( persister ) : - null; + snapshot = persister.isMutable() ? collection.getSnapshot( persister ) : null; collection.setSnapshot( loadedKey, role, snapshot ); } @@ -123,7 +123,7 @@ public CollectionEntry(PersistentCollection collection, SessionFactoryImpleme loadedKey = collection.getKey(); role = collection.getRole(); - loadedPersister = factory.getRuntimeMetamodels().getMappingMetamodel().getCollectionDescriptor( NullnessUtil.castNonNull( role ) ); + loadedPersister = factory.getMappingMetamodel().getCollectionDescriptor( castNonNull( role ) ); snapshot = collection.getStoredSnapshot(); } @@ -156,12 +156,11 @@ private void dirty(PersistentCollection collection) throws HibernateException final CollectionPersister loadedPersister = getLoadedPersister(); final boolean forceDirty = collection.wasInitialized() - && !collection.isDirty() //optimization - && loadedPersister != null - && loadedPersister.isMutable() //optimization - && ( collection.isDirectlyAccessible() || loadedPersister.getElementType().isMutable() ) //optimization - && !collection.equalsSnapshot( loadedPersister ); - + && !collection.isDirty() //optimization + && loadedPersister != null + && loadedPersister.isMutable() //optimization + && ( collection.isDirectlyAccessible() || loadedPersister.getElementType().isMutable() ) //optimization + && !collection.equalsSnapshot( loadedPersister ); if ( forceDirty ) { collection.dirty(); } @@ -174,23 +173,19 @@ public void preFlush(PersistentCollection collection) throws HibernateExcepti } final CollectionPersister loadedPersister = getLoadedPersister(); - boolean nonMutableChange = collection.isDirty() - && loadedPersister != null - && !loadedPersister.isMutable(); + final boolean nonMutableChange = + collection.isDirty() + && loadedPersister != null + && !loadedPersister.isMutable(); if ( nonMutableChange ) { - throw new HibernateException( - "changed an immutable collection instance: " + - collectionInfoString( NullnessUtil.castNonNull( loadedPersister ).getRole(), getLoadedKey() ) - ); + throw new HibernateException( "changed an immutable collection instance: " + + collectionInfoString( castNonNull( loadedPersister ).getRole(), getLoadedKey() ) ); } dirty( collection ); if ( LOG.isDebugEnabled() && collection.isDirty() && loadedPersister != null ) { - LOG.debugf( - "Collection dirty: %s", - collectionInfoString( loadedPersister.getRole(), getLoadedKey() ) - ); + LOG.debug( "Collection dirty: " + collectionInfoString( loadedPersister.getRole(), getLoadedKey() ) ); } setReached( false ); @@ -208,9 +203,9 @@ public void postInitialize(PersistentCollection collection, SharedSessionCont ? collection.getSnapshot( loadedPersister ) : null; collection.setSnapshot( loadedKey, role, snapshot ); - if ( loadedPersister != null && session.getLoadQueryInfluencers().effectivelyBatchLoadable( loadedPersister ) ) { - session.getPersistenceContextInternal() - .getBatchFetchQueue() + if ( loadedPersister != null + && session.getLoadQueryInfluencers().effectivelyBatchLoadable( loadedPersister ) ) { + session.getPersistenceContextInternal().getBatchFetchQueue() .removeBatchLoadableCollection( this ); } } @@ -236,12 +231,12 @@ public void afterAction(PersistentCollection collection) { loadedKey = getCurrentKey(); setLoadedPersister( getCurrentPersister() ); - boolean resnapshot = collection.wasInitialized() + final boolean resnapshot = collection.wasInitialized() && ( isDoremove() || isDorecreate() || isDoupdate() ); if ( resnapshot ) { - snapshot = loadedPersister == null || !loadedPersister.isMutable() ? - null : - collection.getSnapshot( NullnessUtil.castNonNull( loadedPersister ) ); //re-snapshot + snapshot = loadedPersister != null && loadedPersister.isMutable() + ? collection.getSnapshot( castNonNull( loadedPersister ) ) + : null; //re-snapshot } collection.postAction(); @@ -271,13 +266,11 @@ public void afterAction(PersistentCollection collection) { public void resetStoredSnapshot(PersistentCollection collection, Serializable storedSnapshot) { LOG.debugf("Reset storedSnapshot to %s for %s", storedSnapshot, this); - if ( fromMerge ) { - return; // EARLY EXIT! + if ( !fromMerge ) { + snapshot = storedSnapshot; + collection.setSnapshot( loadedKey, role, snapshot ); + fromMerge = true; } - - snapshot = storedSnapshot; - collection.setSnapshot( loadedKey, role, snapshot ); - fromMerge = true; } private void setLoadedPersister(@Nullable CollectionPersister persister) { @@ -286,12 +279,9 @@ private void setLoadedPersister(@Nullable CollectionPersister persister) { } void afterDeserialize(@Nullable SessionFactoryImplementor factory) { - if ( factory == null ) { - loadedPersister = null; - } - else { - loadedPersister = factory.getRuntimeMetamodels().getMappingMetamodel().getCollectionDescriptor( NullnessUtil.castNonNull( role ) ); - } + loadedPersister = factory == null ? null + : factory.getRuntimeMetamodels().getMappingMetamodel() + .getCollectionDescriptor( castNonNull( role ) ); } public boolean wasDereferenced() { @@ -379,13 +369,15 @@ public void setRole(@Nullable String role) { @Override public String toString() { - String result = "CollectionEntry" + - collectionInfoString( role, loadedKey ); - if ( currentPersister != null ) { - result += "->" + - collectionInfoString( currentPersister.getRole(), currentKey ); + final StringBuilder result = + new StringBuilder( "CollectionEntry" ) + .append( collectionInfoString( role, loadedKey ) ); + final CollectionPersister persister = currentPersister; + if ( persister != null ) { + result.append( "->" ) + .append( collectionInfoString( persister.getRole(), currentKey ) ); } - return result; + return result.toString(); } /** @@ -432,9 +424,8 @@ public void serialize(ObjectOutputStream oos) throws IOException { * * @return The deserialized CollectionEntry */ - public static CollectionEntry deserialize( - ObjectInputStream ois, - SessionImplementor session) throws IOException, ClassNotFoundException { + public static CollectionEntry deserialize(ObjectInputStream ois, SessionImplementor session) + throws IOException, ClassNotFoundException { return new CollectionEntry( (String) ois.readObject(), (Serializable) ois.readObject(), diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/Status.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/Status.java index 3a39ff575626..223762a5c7c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/Status.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/Status.java @@ -7,8 +7,9 @@ /** * Represents the status of an entity with respect to * this session. These statuses are for internal - * book-keeping only and are not intended to represent - * any notion that is visible to the _application_. + * bookkeeping only and are not intended to represent + * any notion that is visible to the application + * program. */ public enum Status { MANAGED, diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java index 1e1dd72749e5..f622ca6fb7b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java @@ -19,7 +19,6 @@ import org.hibernate.engine.internal.Collections; import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.spi.ActionQueue; -import org.hibernate.engine.spi.CascadingAction; import org.hibernate.engine.spi.CascadingActions; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.CollectionKey; @@ -57,44 +56,37 @@ public abstract class AbstractFlushingEventListener { /** * Coordinates the processing necessary to get things ready for executions - * as db calls by preping the session caches and moving the appropriate + * as db calls by preparing the session caches and moving the appropriate * entities and collections to their respective execution queues. * * @param event The flush event. * @throws HibernateException Error flushing caches to execution queues. */ protected void flushEverythingToExecutions(FlushEvent event) throws HibernateException { - LOG.trace( "Flushing session" ); - final EventSource session = event.getSession(); - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); preFlush( session, persistenceContext ); - flushEverythingToExecutions( event, persistenceContext, session ); } protected void flushEverythingToExecutions(FlushEvent event, PersistenceContext persistenceContext, EventSource session) { persistenceContext.setFlushing( true ); try { - int entityCount = flushEntities( event, persistenceContext ); - int collectionCount = flushCollections( session, persistenceContext ); - + final int entityCount = flushEntities( event, persistenceContext ); + final int collectionCount = flushCollections( session, persistenceContext ); event.setNumberOfEntitiesProcessed( entityCount ); event.setNumberOfCollectionsProcessed( collectionCount ); } finally { persistenceContext.setFlushing( false); } - //some statistics logFlushResults( event ); } protected void preFlush(EventSource session, PersistenceContext persistenceContext) { session.getInterceptor().preFlush( persistenceContext.managedEntitiesIterator() ); - prepareEntityFlushes( session, persistenceContext ); // we could move this inside if we wanted to // tolerate collection initializations during @@ -106,29 +98,27 @@ protected void preFlush(EventSource session, PersistenceContext persistenceConte } protected void logFlushResults(FlushEvent event) { - if ( !LOG.isDebugEnabled() ) { - return; + if ( LOG.isDebugEnabled() ) { + final EventSource session = event.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + final ActionQueue actionQueue = session.getActionQueue(); + LOG.debugf( + "Flushed: %s insertions, %s updates, %s deletions to %s objects", + actionQueue.numberOfInsertions(), + actionQueue.numberOfUpdates(), + actionQueue.numberOfDeletions(), + persistenceContext.getNumberOfManagedEntities() + ); + LOG.debugf( + "Flushed: %s (re)creations, %s updates, %s removals to %s collections", + actionQueue.numberOfCollectionCreations(), + actionQueue.numberOfCollectionUpdates(), + actionQueue.numberOfCollectionRemovals(), + persistenceContext.getCollectionEntriesSize() + ); + new EntityPrinter( session.getFactory() ) + .logEntities( persistenceContext.getEntityHoldersByKey().entrySet() ); } - final EventSource session = event.getSession(); - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - final ActionQueue actionQueue = session.getActionQueue(); - LOG.debugf( - "Flushed: %s insertions, %s updates, %s deletions to %s objects", - actionQueue.numberOfInsertions(), - actionQueue.numberOfUpdates(), - actionQueue.numberOfDeletions(), - persistenceContext.getNumberOfManagedEntities() - ); - LOG.debugf( - "Flushed: %s (re)creations, %s updates, %s removals to %s collections", - actionQueue.numberOfCollectionCreations(), - actionQueue.numberOfCollectionUpdates(), - actionQueue.numberOfCollectionRemovals(), - persistenceContext.getCollectionEntriesSize() - ); - new EntityPrinter( session.getFactory() ).toString( - persistenceContext.getEntityHoldersByKey().entrySet() - ); } /** @@ -137,11 +127,9 @@ protected void logFlushResults(FlushEvent event) { * and also apply orphan delete */ private void prepareEntityFlushes(EventSource session, PersistenceContext persistenceContext) throws HibernateException { - LOG.debug( "Processing flush-time cascades" ); - - final PersistContext context = getContext( session ); - //safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap + final PersistContext context = PersistContext.create(); + // safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap for ( Map.Entry me : persistenceContext.reentrantSafeEntityEntries() ) { // for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) { final EntityEntry entry = me.getValue(); @@ -149,7 +137,10 @@ private void prepareEntityFlushes(EventSource session, PersistenceContext persis cascadeOnFlush( session, entry.getPersister(), me.getKey(), context ); } } + checkForTransientReferences( session, persistenceContext ); + } + void checkForTransientReferences(EventSource session, PersistenceContext persistenceContext) { // perform these checks after all cascade persist events have been // processed, so that all entities which will be persisted are // persistent when we do the check (I wonder if we could move this @@ -181,76 +172,61 @@ private void cascadeOnFlush(EventSource session, EntityPersister persister, Obje final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); persistenceContext.incrementCascadeLevel(); try { - Cascade.cascade( getCascadingAction(session), CascadePoint.BEFORE_FLUSH, session, persister, object, anything ); + Cascade.cascade( CascadingActions.PERSIST_ON_FLUSH, CascadePoint.BEFORE_FLUSH, session, persister, object, anything ); } finally { persistenceContext.decrementCascadeLevel(); } } - protected PersistContext getContext(EventSource session) { - return PersistContext.create(); - } - - protected CascadingAction getCascadingAction(EventSource session) { - return CascadingActions.PERSIST_ON_FLUSH; - } - /** - * Initialize the flags of the CollectionEntry, including the - * dirty check. + * Initialize the flags of the {@link CollectionEntry}, including the dirty check. */ private void prepareCollectionFlushes(PersistenceContext persistenceContext) throws HibernateException { - // Initialize dirty flags for arrays + collections with composite elements // and reset reached, doupdate, etc. - LOG.debug( "Dirty checking collections" ); - final Map, CollectionEntry> collectionEntries = persistenceContext.getCollectionEntries(); + final Map, CollectionEntry> collectionEntries = + persistenceContext.getCollectionEntries(); if ( collectionEntries != null ) { - for ( Map.Entry, CollectionEntry> entry : ( (IdentityMap, CollectionEntry>) collectionEntries ).entryArray() ) { + for ( Map.Entry, CollectionEntry> entry : + ( (IdentityMap, CollectionEntry>) collectionEntries ).entryArray() ) { entry.getValue().preFlush( entry.getKey() ); } } } /** - * 1. detect any dirty entities - * 2. schedule any entity updates - * 3. search out any reachable collections + *
    + *
  1. detect any dirty entities + *
  2. schedule any entity updates + *
  3. search out any reachable collections + *
*/ private int flushEntities(final FlushEvent event, final PersistenceContext persistenceContext) throws HibernateException { - LOG.trace( "Flushing entities and processing referenced collections" ); final EventSource source = event.getSession(); final EventListenerGroup flushListeners = event.getFactory().getFastSessionServices().eventListenerGroup_FLUSH_ENTITY; - // Among other things, updateReachables() will recursively load all - // collections that are moving roles. This might cause entities to - // be loaded. - + // Among other things, updateReachables() recursively loads all + // collections that are changing roles. This might cause entities + // to be loaded. // So this needs to be safe from concurrent modification problems. - final Map.Entry[] entityEntries = persistenceContext.reentrantSafeEntityEntries(); final int count = entityEntries.length; FlushEntityEvent entityEvent = null; //allow reuse of the event as it's heavily allocated in certain use cases int eventGenerationId = 0; //Used to double-check the instance reuse won't cause problems - for ( Map.Entry me : entityEntries ) { // Update the status of the object and if necessary, schedule an update - final EntityEntry entry = me.getValue(); final Status status = entry.getStatus(); - if ( status != Status.LOADING && status != Status.GONE ) { entityEvent = createOrReuseEventInstance( entityEvent, source, me.getKey(), entry ); - entityEvent.setInstanceGenerationId( ++eventGenerationId ); - flushListeners.fireEventOnEachListener( entityEvent, FlushEntityEventListener::onFlushEntity ); entityEvent.setAllowedToReuse( true ); assert entityEvent.getInstanceGenerationId() == eventGenerationId; @@ -263,8 +239,8 @@ private int flushEntities(final FlushEvent event, final PersistenceContext persi } /** - * Reuses a FlushEntityEvent for a new purpose, if possible; - * if not possible a new actual instance is returned. + * Reuses a {@link FlushEntityEvent} for a new purpose, if possible; + * or if not possible, a new actual instance is returned. */ private FlushEntityEvent createOrReuseEventInstance( FlushEntityEvent possiblyValidExistingInstance, @@ -282,13 +258,12 @@ private FlushEntityEvent createOrReuseEventInstance( } /** - * process any unreferenced collections and then inspect all known collections, - * scheduling creates/removes/updates + * Process any unreferenced collections and then inspect all known collections, + * scheduling creates/removes/updates. */ private int flushCollections(final EventSource session, final PersistenceContext persistenceContext) throws HibernateException { LOG.trace( "Processing unreferenced collections" ); - final Map, CollectionEntry> collectionEntries = persistenceContext.getCollectionEntries(); final int count; if ( collectionEntries == null ) { @@ -307,7 +282,6 @@ private int flushCollections(final EventSource session, final PersistenceContext // Schedule updates to collections: LOG.trace( "Scheduling collection removes/(re)creates/updates" ); - final ActionQueue actionQueue = session.getActionQueue(); final Interceptor interceptor = session.getInterceptor(); persistenceContext.forEachCollectionEntry( @@ -409,12 +383,13 @@ protected void performExecutions(EventSource session) { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** - * 1. Recreate the collection key -> collection map - * 2. rebuild the collection entries - * 3. call Interceptor.postFlush() + *
    + *
  1. Recreate the collection key to collection mapping + *
  2. rebuild the collection entries + *
  3. call {@link Interceptor#postFlush} + *
*/ protected void postFlush(SessionImplementor session) throws HibernateException { - LOG.trace( "Post flush" ); final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); @@ -436,13 +411,12 @@ protected void postFlush(SessionImplementor session) throws HibernateException { } else { //otherwise recreate the mapping between the collection and its key - final CollectionKey collectionKey = new CollectionKey( - collectionEntry.getLoadedPersister(), - key - ); + final CollectionKey collectionKey = + new CollectionKey( collectionEntry.getLoadedPersister(), key ); persistenceContext.addCollectionByKey( collectionKey, persistentCollection ); } - }, true + }, + true ); } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java index 4f8288db5639..206594ad9795 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java @@ -60,11 +60,11 @@ void processValue(int i, Object[] values, Type[] types) { } boolean includeEntityProperty(Object[] values, int i) { - return includeProperty(values, i); + return includeProperty( values, i ); } boolean includeProperty(Object[] values, int i) { - return values[i]!= LazyPropertyInitializer.UNFETCHED_PROPERTY; + return values[i] != LazyPropertyInitializer.UNFETCHED_PROPERTY; } /** @@ -72,7 +72,7 @@ boolean includeProperty(Object[] values, int i) { * to processValue(). */ Object processComponent(Object component, CompositeType componentType) throws HibernateException { - if ( component!=null ) { + if ( component != null ) { processValues( componentType.getPropertyValues(component, session), componentType.getSubtypes() ); } return null; diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java index 8c92a9cab41e..8147dcf3e390 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java @@ -5,45 +5,87 @@ package org.hibernate.event.internal; import org.hibernate.HibernateException; -import org.hibernate.engine.spi.ActionQueue; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityHolder; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.DirtyCheckEvent; import org.hibernate.event.spi.DirtyCheckEventListener; -import org.hibernate.internal.CoreLogging; -import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.event.spi.EventSource; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.entity.EntityPersister; + /** - * Defines the default dirty-check event listener used by hibernate for - * checking the session for dirtiness in response to generated dirty-check - * events. + * Determines if the current session holds modified state which + * would be synchronized with the database if the session were + * flushed. Approximately reproduces the logic that is executed + * on an {@link org.hibernate.event.spi.AutoFlushEvent}. + * + * @implNote Historically, {@link org.hibernate.Session#isDirty} + * was implemented to actually execute a partial flush and then + * return {@code true} if there were any resulting operations to + * be executed. This was extremely inefficient and side-effecty. + * The new implementation below is non-perfect and probably + * misses some subtleties, but it's I guess OK to view it as an + * approximation. * - * @author Steve Ebersole + * @author Gavin King */ -public class DefaultDirtyCheckEventListener extends AbstractFlushingEventListener implements DirtyCheckEventListener { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( DefaultDirtyCheckEventListener.class ); +public class DefaultDirtyCheckEventListener implements DirtyCheckEventListener { - /** - * Handle the given dirty-check event. - * - * @param event The dirty-check event to be handled. - */ @Override public void onDirtyCheck(DirtyCheckEvent event) throws HibernateException { - final ActionQueue actionQueue = event.getSession().getActionQueue(); - int oldSize = actionQueue.numberOfCollectionRemovals(); - - try { - flushEverythingToExecutions(event); - boolean wasNeeded = actionQueue.hasAnyQueuedActions(); - if ( wasNeeded ) { - LOG.debug( "Session dirty" ); + final EventSource session = event.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContext(); + final var holdersByKey = persistenceContext.getEntityHoldersByKey(); + if ( holdersByKey != null ) { + for ( var entry : holdersByKey.entrySet() ) { + final EntityHolder holder = entry.getValue(); + final EntityEntry entityEntry = holder.getEntityEntry(); + final Status status = entityEntry.getStatus(); + if ( status != Status.MANAGED && status != Status.GONE + || isEntityDirty( holder.getManagedObject(), holder.getDescriptor(), entityEntry, session ) ) { + event.setDirty( true ); + return; + } } - else { - LOG.debug( "Session not dirty" ); + } + final var entriesByCollection = persistenceContext.getCollectionEntries(); + if ( entriesByCollection != null ) { + for ( var entry : entriesByCollection.entrySet() ) { + if ( isCollectionDirty( entry.getKey(), entry.getValue().getLoadedPersister() ) ) { + event.setDirty( true ); + return; + } } - event.setDirty( wasNeeded ); } - finally { - actionQueue.clearFromFlushNeededCheck( oldSize ); + } + + private static boolean isEntityDirty( + Object entity, EntityPersister descriptor, EntityEntry entityEntry, EventSource session) { + if ( entityEntry.requiresDirtyCheck( entity ) ) { // takes into account CustomEntityDirtinessStrategy + final Object[] propertyValues = + entityEntry.getStatus() == Status.DELETED + ? entityEntry.getDeletedState() + : descriptor.getValues( entity ); + final int[] dirty = + descriptor.findDirty( propertyValues, entityEntry.getLoadedState(), entity, session ); + return dirty != null; + } + else { + return false; } } + + private static boolean isCollectionDirty(PersistentCollection collection, CollectionPersister loadedPersister) { + return collection.isDirty() + || collection.wasInitialized() + && loadedPersister != null + && loadedPersister.isMutable() //optimization +// && !loadedPersister.isInverse() // even if it's inverse, could still result in a cache update + && ( collection.isDirectlyAccessible() || loadedPersister.getElementType().isMutable() ) //optimization + && !collection.equalsSnapshot( loadedPersister ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java index 53f249921120..fde3d8c3076a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java @@ -30,7 +30,6 @@ import org.hibernate.event.spi.FlushEntityEventListener; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.jpa.event.spi.CallbackRegistry; import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; import org.hibernate.metamodel.mapping.NaturalIdMapping; @@ -49,6 +48,7 @@ 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.internal.util.collections.ArrayHelper.EMPTY_INT_ARRAY; import static org.hibernate.pretty.MessageHelper.infoString; /** @@ -162,21 +162,23 @@ public void onFlushEntity(FlushEntityEvent event) throws HibernateException { } private Object[] getValues(Object entity, EntityEntry entry, boolean mightBeDirty, SessionImplementor session) { - final Object[] loadedState = entry.getLoadedState(); if ( entry.getStatus() == Status.DELETED ) { //grab its state saved at deletion return entry.getDeletedState(); } - else if ( !mightBeDirty && loadedState != null ) { - return loadedState; - } else { - final EntityPersister persister = entry.getPersister(); - checkId( entity, persister, entry.getId(), entry.getStatus(), session ); - // grab its current state - final Object[] values = persister.getValues( entity ); - checkNaturalId( persister, entity, entry, values, loadedState, session ); - return values; + final Object[] loadedState = entry.getLoadedState(); + if ( !mightBeDirty && loadedState != null ) { + return loadedState; + } + else { + final EntityPersister persister = entry.getPersister(); + checkId( entity, persister, entry.getId(), entry.getStatus(), session ); + // grab its current state + final Object[] values = persister.getValues( entity ); + checkNaturalId( persister, entity, entry, values, loadedState, session ); + return values; + } } } @@ -282,7 +284,7 @@ private static int[] getDirtyProperties(FlushEntityEvent event, boolean intercep } else { // it was dirtied by a collection only - return ArrayHelper.EMPTY_INT_ARRAY; + return EMPTY_INT_ARRAY; } } else { @@ -290,7 +292,8 @@ private static int[] getDirtyProperties(FlushEntityEvent event, boolean intercep } } - private static void logScheduleUpdate(EntityEntry entry, SessionFactoryImplementor factory, Status status, EntityPersister persister) { + private static void logScheduleUpdate( + EntityEntry entry, SessionFactoryImplementor factory, Status status, EntityPersister persister) { if ( LOG.isTraceEnabled() ) { final String info = infoString( persister, entry.getId(), factory ); if ( status == Status.DELETED ) { @@ -326,24 +329,15 @@ protected boolean invokeInterceptor(FlushEntityEvent event) { final Object id = entry.getId(); final Object[] values = event.getPropertyValues(); final EntityPersister persister = entry.getPersister(); - final EventSource session = event.getSession(); - final boolean isDirty; - if ( entry.getStatus() != Status.DELETED && callbackRegistry.preUpdate( entity ) ) { - isDirty = copyState( entity, persister.getPropertyTypes(), values, event.getFactory() ); - } - else { - isDirty = false; - } + final boolean isDirty = + entry.getStatus() != Status.DELETED && callbackRegistry.preUpdate( entity ) + && copyState( entity, persister.getPropertyTypes(), values, event.getFactory() ); - final boolean stateModified = session.getInterceptor().onFlushDirty( - entity, - id, - values, - entry.getLoadedState(), - persister.getPropertyNames(), - persister.getPropertyTypes() - ); + final boolean stateModified = + event.getSession().getInterceptor() + .onFlushDirty( entity, id, values, entry.getLoadedState(), + persister.getPropertyNames(), persister.getPropertyTypes() ); return stateModified || isDirty; } @@ -385,9 +379,10 @@ private Object getNextVersion(FlushEntityEvent event) throws HibernateException return getVersion( values, persister ); } else { - final Object nextVersion = isVersionIncrementRequired( event, entry ) - ? incrementVersion( event.getEntity(), entry.getVersion(), persister, event.getSession() ) - : entry.getVersion(); //use the current version + final Object nextVersion = + isVersionIncrementRequired( event, entry ) + ? incrementVersion( event.getEntity(), entry.getVersion(), persister, event.getSession() ) + : entry.getVersion(); //use the current version setVersion( values, nextVersion, persister ); return nextVersion; } @@ -420,20 +415,14 @@ private static boolean isVersionIncrementRequired(FlushEntityEvent event, Entity protected final boolean isUpdateNecessary(FlushEntityEvent event) throws HibernateException { return !event.isDirtyCheckPossible() || event.hasDirtyProperties() - || hasDirtyCollections(event); + || hasDirtyCollections( event ); } 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() - ); - visitor.processEntityPropertyValues( event.getPropertyValues(), persister.getPropertyTypes() ); - final boolean hasDirtyCollections = visitor.wasDirtyCollectionFound(); + final boolean hasDirtyCollections = hasDirtyCollections( event, persister ); event.setHasDirtyCollection( hasDirtyCollections ); return hasDirtyCollections; } @@ -442,6 +431,14 @@ private boolean hasDirtyCollections(FlushEntityEvent event) { } } + private static boolean hasDirtyCollections(FlushEntityEvent event, EntityPersister persister) { + final DirtyCollectionSearchVisitor visitor = + new DirtyCollectionSearchVisitor( event.getEntity(), event.getSession(), + persister.getPropertyVersionability() ); + visitor.processEntityPropertyValues( event.getPropertyValues(), persister.getPropertyTypes() ); + return visitor.wasDirtyCollectionFound(); + } + private boolean isCollectionDirtyCheckNecessary(EntityPersister persister, Status status) { return ( status == Status.MANAGED || status == Status.READ_ONLY ) && persister.isVersioned() @@ -488,13 +485,11 @@ private static int[] performDirtyCheck(FlushEntityEvent event) { 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. + // A non-modifiable (e.g., read-only or immutable) entity needs to 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" - ); + throw new IllegalStateException( "Entity has status DELETED but values != entry.getDeletedState" ); } // Even if loadedState == null, we can dirty-check by comparing currentState and // entry.getDeletedState() because the only fields to be updated are those that @@ -581,12 +576,9 @@ public void doDirtyChecking(CustomEntityDirtinessStrategy.AttributeChecker attri 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 resolveDirtyAttributeIndex( tracker, event, persister, entry ); - } - else { - return ArrayHelper.EMPTY_INT_ARRAY; - } + return tracker.$$_hibernate_hasDirtyAttributes() || persister.hasMutableProperties() + ? resolveDirtyAttributeIndex( tracker, event, persister, entry ) + : EMPTY_INT_ARRAY; } private static int[] resolveDirtyAttributeIndex( diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 294af9e455b0..de6af4828d74 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -396,7 +396,7 @@ private ClearEvent createClearEvent() { } @Override - public void close() throws HibernateException { + public void close() { if ( isClosed() ) { if ( getFactory().getSessionFactoryOptions().getJpaCompliance().isJpaClosedComplianceEnabled() ) { throw new IllegalStateException( "EntityManager was already closed" ); @@ -408,7 +408,7 @@ public void close() throws HibernateException { } } - public void closeWithoutOpenChecks() throws HibernateException { + public void closeWithoutOpenChecks() { if ( log.isTraceEnabled() ) { log.tracef( "Closing session [%s]", getSessionIdentifier() ); } @@ -560,7 +560,7 @@ protected void cleanupOnClose() { } @Override - public LockMode getCurrentLockMode(Object object) throws HibernateException { + public LockMode getCurrentLockMode(Object object) { checkOpen(); checkTransactionSynchStatus(); if ( object == null ) { @@ -589,7 +589,7 @@ public LockMode getCurrentLockMode(Object object) throws HibernateException { } @Override - public Object getEntityUsingInterceptor(EntityKey key) throws HibernateException { + public Object getEntityUsingInterceptor(EntityKey key) { checkOpenOrWaitingForAutoClose(); // todo : should this get moved to PersistentContext? // logically, is PersistentContext the "thing" to which an interceptor gets attached? @@ -649,7 +649,7 @@ public void lock(String entityName, Object object, LockOptions lockOptions) { } @Override - public void lock(Object object, LockMode lockMode) throws HibernateException { + public void lock(Object object, LockMode lockMode) { final LockOptions lockOptions = copySessionLockOptions(); lockOptions.setLockMode( lockMode ); fireLock( new LockEvent( object, lockOptions, this ) ); @@ -690,19 +690,19 @@ else if ( exception instanceof MappingException ) { // persist() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override - public void persist(String entityName, Object object) throws HibernateException { + public void persist(String entityName, Object object) { checkOpen(); firePersist( new PersistEvent( entityName, object, this ) ); } @Override - public void persist(Object object) throws HibernateException { + public void persist(Object object) { checkOpen(); firePersist( new PersistEvent( null, object, this ) ); } @Override - public void persist(String entityName, Object object, PersistContext copiedAlready) throws HibernateException { + public void persist(String entityName, Object object, PersistContext copiedAlready) { checkOpenOrWaitingForAutoClose(); firePersist( copiedAlready, new PersistEvent( entityName, object, this ) ); } @@ -784,19 +784,19 @@ public void persistOnFlush(String entityName, Object object, PersistContext copi // merge() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override @SuppressWarnings("unchecked") - public T merge(String entityName, T object) throws HibernateException { + public T merge(String entityName, T object) { checkOpen(); return (T) fireMerge( new MergeEvent( entityName, object, this ) ); } @Override @SuppressWarnings("unchecked") - public T merge(T object) throws HibernateException { + public T merge(T object) { checkOpen(); return (T) fireMerge( new MergeEvent( null, object, this )); } @Override - public void merge(String entityName, Object object, MergeContext copiedAlready) throws HibernateException { + public void merge(String entityName, Object object, MergeContext copiedAlready) { checkOpenOrWaitingForAutoClose(); fireMerge( copiedAlready, new MergeEvent( entityName, object, this ) ); } @@ -848,8 +848,7 @@ private void fireMerge(final MergeContext mergeContext, final MergeEvent event) // delete() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override - public void delete(String entityName, Object object, boolean isCascadeDeleteEnabled, DeleteContext transientEntities) - throws HibernateException { + public void delete(String entityName, Object object, boolean isCascadeDeleteEnabled, DeleteContext transientEntities) { checkOpenOrWaitingForAutoClose(); final boolean removingOrphanBeforeUpdates = persistenceContext.isRemovingOrphanBeforeUpates(); final boolean traceEnabled = log.isTraceEnabled(); @@ -945,7 +944,7 @@ private void fireDelete(final DeleteEvent event, final DeleteContext transientEn // load()/get() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override - public void load(Object object, Object id) throws HibernateException { + public void load(Object object, Object id) { fireLoad( new LoadEvent( id, object, this, getReadOnlyFromLoadQueryInfluencers() ), LoadEventListener.RELOAD ); } @@ -1003,12 +1002,12 @@ public List findMultiple(Class entityType, List ids, FindOptio } @Override - public T get(Class entityClass, Object id) throws HibernateException { + public T get(Class entityClass, Object id) { return byId( entityClass ).load( id ); } @Override - public Object get(String entityName, Object id) throws HibernateException { + public Object get(String entityName, Object id) { return byId( entityName ).load( id ); } @@ -1018,7 +1017,7 @@ public Object get(String entityName, Object id) throws HibernateException { * Do NOT return a proxy. */ @Override - public Object immediateLoad(String entityName, Object id) throws HibernateException { + public Object immediateLoad(String entityName, Object id) { if ( log.isDebugEnabled() ) { final EntityPersister persister = requireEntityPersister( entityName ); log.debugf( "Initializing proxy: %s", infoString( persister, id, getFactory() ) ); @@ -1170,22 +1169,22 @@ private void releaseLoadEvent(LoadEvent event) { } @Override - public T get(Class entityClass, Object id, LockMode lockMode) throws HibernateException { + public T get(Class entityClass, Object id, LockMode lockMode) { return this.byId( entityClass ).with( new LockOptions( lockMode ) ).load( id ); } @Override - public T get(Class entityClass, Object id, LockOptions lockOptions) throws HibernateException { + public T get(Class entityClass, Object id, LockOptions lockOptions) { return this.byId( entityClass ).with( lockOptions ).load( id ); } @Override - public Object get(String entityName, Object id, LockMode lockMode) throws HibernateException { + public Object get(String entityName, Object id, LockMode lockMode) { return this.byId( entityName ).with( new LockOptions( lockMode ) ).load( id ); } @Override - public Object get(String entityName, Object id, LockOptions lockOptions) throws HibernateException { + public Object get(String entityName, Object id, LockOptions lockOptions) { return this.byId( entityName ).with( lockOptions ).load( id ); } @@ -1299,24 +1298,24 @@ private void fireResolveNaturalId(final ResolveNaturalIdEvent event) { // refresh() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override - public void refresh(Object object) throws HibernateException { + public void refresh(Object object) { fireRefresh( new RefreshEvent( object, this ) ); } @Override - public void refresh(Object object, LockMode lockMode) throws HibernateException { + public void refresh(Object object, LockMode lockMode) { final LockOptions lockOptions = copySessionLockOptions(); lockOptions.setLockMode( lockMode ); fireRefresh( new RefreshEvent( object, lockOptions, this ) ); } @Override - public void refresh(Object object, LockOptions lockOptions) throws HibernateException { + public void refresh(Object object, LockOptions lockOptions) { fireRefresh( new RefreshEvent( object, lockOptions, this ) ); } @Override - public void refresh(String entityName, Object object, RefreshContext refreshedAlready) throws HibernateException { + public void refresh(String entityName, Object object, RefreshContext refreshedAlready) { fireRefresh( refreshedAlready, new RefreshEvent( entityName, object, this ) ); } @@ -1374,13 +1373,12 @@ private boolean managed(String entityName, Object entity) { // replicate() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override - public void replicate(Object obj, ReplicationMode replicationMode) throws HibernateException { + public void replicate(Object obj, ReplicationMode replicationMode) { fireReplicate( new ReplicateEvent( obj, replicationMode, this ) ); } @Override - public void replicate(String entityName, Object obj, ReplicationMode replicationMode) - throws HibernateException { + public void replicate(String entityName, Object obj, ReplicationMode replicationMode) { fireReplicate( new ReplicateEvent( entityName, obj, replicationMode, this ) ); } @@ -1400,7 +1398,7 @@ private void fireReplicate(final ReplicateEvent event) { * (references held by application or other persistent instances are okay) */ @Override - public void evict(Object object) throws HibernateException { + public void evict(Object object) { checkOpen(); pulseTransactionCoordinator(); final EvictEvent event = new EvictEvent( object, this ); @@ -1410,13 +1408,12 @@ public void evict(Object object) throws HibernateException { } @Override - public boolean autoFlushIfRequired(Set querySpaces) throws HibernateException { + public boolean autoFlushIfRequired(Set querySpaces) { return autoFlushIfRequired( querySpaces, false ); } @Override - public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush) - throws HibernateException { + public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush) { checkOpen(); if ( !isTransactionInProgress() ) { // do not auto-flush while outside a transaction @@ -1439,26 +1436,22 @@ public void autoPreFlush(){ .fireEventOnEachListener( this, AutoFlushEventListener::onAutoPreFlush ); } - - @Override - public boolean isDirty() throws HibernateException { + public boolean isDirty() { checkOpen(); - pulseTransactionCoordinator(); - log.debug( "Checking session dirtiness" ); if ( actionQueue.areInsertionsOrDeletionsQueued() ) { - log.debug( "Session dirty (scheduled updates and insertions)" ); return true; } - DirtyCheckEvent event = new DirtyCheckEvent( this ); - fastSessionServices.eventListenerGroup_DIRTY_CHECK - .fireEventOnEachListener( event, DirtyCheckEventListener::onDirtyCheck ); - delayedAfterCompletion(); - return event.isDirty(); + else { + final DirtyCheckEvent event = new DirtyCheckEvent( this ); + fastSessionServices.eventListenerGroup_DIRTY_CHECK + .fireEventOnEachListener( event, DirtyCheckEventListener::onDirtyCheck ); + return event.isDirty(); + } } @Override - public void flush() throws HibernateException { + public void flush() { checkOpen(); doFlush(); } @@ -1486,17 +1479,15 @@ public void setFlushMode(FlushModeType flushModeType) { } @Override - public void forceFlush(EntityEntry entityEntry) throws HibernateException { + public void forceFlush(EntityEntry entityEntry) { forceFlush( entityEntry.getEntityKey() ); } @Override - public void forceFlush(EntityKey key) throws HibernateException { + public void forceFlush(EntityKey key) { if ( log.isDebugEnabled() ) { - log.debugf( - "Flushing to force deletion of re-saved object: %s", - infoString( key.getPersister(), key.getIdentifier(), getFactory() ) - ); + log.debugf("Flushing to force deletion of re-saved object: " + + infoString( key.getPersister(), key.getIdentifier(), getFactory() ) ); } if ( persistenceContext.getCascadeLevel() > 0 ) { @@ -1511,7 +1502,7 @@ public void forceFlush(EntityKey key) throws HibernateException { } @Override - public Object instantiate(String entityName, Object id) throws HibernateException { + public Object instantiate(String entityName, Object id) { return instantiate( requireEntityPersister( entityName ), id ); } @@ -1519,7 +1510,7 @@ public Object instantiate(String entityName, Object id) throws HibernateExceptio * give the interceptor an opportunity to override the default instantiation */ @Override - public Object instantiate(EntityPersister persister, Object id) throws HibernateException { + public Object instantiate(EntityPersister persister, Object id) { checkOpenOrWaitingForAutoClose(); pulseTransactionCoordinator(); Object result = getInterceptor() @@ -1560,7 +1551,7 @@ public EntityPersister getEntityPersister(final String entityName, final Object // not for internal use: @Override - public Object getIdentifier(Object object) throws HibernateException { + public Object getIdentifier(Object object) { checkOpen(); checkTransactionSynchStatus(); final LazyInitializer lazyInitializer = extractLazyInitializer( object ); @@ -1851,13 +1842,13 @@ public T getReference(T object) { } @Override - public String guessEntityName(Object object) throws HibernateException { + public String guessEntityName(Object object) { checkOpenOrWaitingForAutoClose(); return getEntityNameResolver().resolveEntityName( object ); } @Override - public void cancelQuery() throws HibernateException { + public void cancelQuery() { checkOpen(); getJdbcCoordinator().cancelLastQuery(); } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index 1db48851111e..b7e1f75878b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -812,7 +812,7 @@ public void refresh(String entityName, Object entity, LockMode lockMode) { } @Override - public Object immediateLoad(String entityName, Object id) throws HibernateException { + public Object immediateLoad(String entityName, Object id) { if ( getPersistenceContextInternal().isLoadFinished() ) { throw new SessionException( "proxies cannot be fetched by a stateless session" ); } @@ -821,8 +821,7 @@ public Object immediateLoad(String entityName, Object id) throws HibernateExcept } @Override - public void initializeCollection(PersistentCollection collection, boolean writing) - throws HibernateException { + public void initializeCollection(PersistentCollection collection, boolean writing) { checkOpen(); final PersistenceContext persistenceContext = getPersistenceContextInternal(); final CollectionEntry ce = persistenceContext.getCollectionEntry( collection ); @@ -854,12 +853,12 @@ public void initializeCollection(PersistentCollection collection, boolean wri } @Override - public Object instantiate(String entityName, Object id) throws HibernateException { + public Object instantiate(String entityName, Object id) { return instantiate( getEntityPersister( entityName ), id ); } @Override - public Object instantiate(EntityPersister persister, Object id) throws HibernateException { + public Object instantiate(EntityPersister persister, Object id) { checkOpen(); return persister.instantiate( id, this ); } @@ -869,7 +868,7 @@ public Object internalLoad( String entityName, Object id, boolean eager, - boolean nullable) throws HibernateException { + boolean nullable) { checkOpen(); final EntityPersister persister = getEntityPersister( entityName ); @@ -1035,7 +1034,7 @@ else if ( association instanceof PersistentCollection collection ) { } @Override - public Object getIdentifier(Object entity) throws HibernateException { + public Object getIdentifier(Object entity) { checkOpen(); return getFactory().getPersistenceUnitUtil().getIdentifier(entity); } @@ -1087,14 +1086,13 @@ public Object getContextEntityIdentifier(Object object) { } @Override - public String guessEntityName(Object entity) throws HibernateException { + public String guessEntityName(Object entity) { checkOpen(); return entity.getClass().getName(); } @Override - public EntityPersister getEntityPersister(String entityName, Object object) - throws HibernateException { + public EntityPersister getEntityPersister(String entityName, Object object) { checkOpen(); return entityName == null ? getEntityPersister( guessEntityName( object ) ) @@ -1102,7 +1100,7 @@ public EntityPersister getEntityPersister(String entityName, Object object) } @Override - public Object getEntityUsingInterceptor(EntityKey key) throws HibernateException { + public Object getEntityUsingInterceptor(EntityKey key) { checkOpen(); final PersistenceContext persistenceContext = getPersistenceContext(); @@ -1134,7 +1132,7 @@ public boolean isDefaultReadOnly() { return false; } - public void setDefaultReadOnly(boolean readOnly) throws HibernateException { + public void setDefaultReadOnly(boolean readOnly) { if ( readOnly ) { throw new UnsupportedOperationException(); } @@ -1173,7 +1171,7 @@ public PersistenceContext getPersistenceContextInternal() { } @Override - public boolean autoFlushIfRequired(Set querySpaces) throws HibernateException { + public boolean autoFlushIfRequired(Set querySpaces) { return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java index 22d37d15b99e..c174cd1e311c 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java @@ -44,42 +44,39 @@ public String toString(String entityName, Object entity) throws HibernateExcepti if ( entityPersister == null || !entityPersister.isInstance( entity ) ) { return entity.getClass().getName(); } - - Map result = new HashMap<>(); - - if ( entityPersister.hasIdentifierProperty() ) { - result.put( - entityPersister.getIdentifierPropertyName(), - entityPersister.getIdentifierType().toLoggableString( - entityPersister.getIdentifier( entity ), - factory - ) - ); - } - - Type[] types = entityPersister.getPropertyTypes(); - String[] names = entityPersister.getPropertyNames(); - Object[] values = entityPersister.getValues( entity ); - for ( int i = 0; i < types.length; i++ ) { - if ( !names[i].startsWith( "_" ) ) { - final String strValue; - if ( values[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { - strValue = values[i].toString(); - } - else if ( !Hibernate.isInitialized( values[i] ) ) { - strValue = ""; - } - else { - strValue = types[i].toLoggableString( values[i], factory ); + else { + final Map result = new HashMap<>(); + if ( entityPersister.hasIdentifierProperty() ) { + result.put( + entityPersister.getIdentifierPropertyName(), + entityPersister.getIdentifierType() + .toLoggableString( entityPersister.getIdentifier( entity ), factory ) + ); + } + final Type[] types = entityPersister.getPropertyTypes(); + final String[] names = entityPersister.getPropertyNames(); + final Object[] values = entityPersister.getValues( entity ); + for ( int i = 0; i < types.length; i++ ) { + if ( !names[i].startsWith( "_" ) ) { + final String strValue; + if ( values[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + strValue = values[i].toString(); + } + else if ( !Hibernate.isInitialized( values[i] ) ) { + strValue = ""; + } + else { + strValue = types[i].toLoggableString( values[i], factory ); + } + result.put( names[i], strValue ); } - result.put( names[i], strValue ); } + return entityName + result; } - return entityName + result; } public String toString(Type[] types, Object[] values) throws HibernateException { - StringBuilder buffer = new StringBuilder(); + final StringBuilder buffer = new StringBuilder(); for ( int i = 0; i < types.length; i++ ) { if ( types[i] != null ) { buffer.append( types[i].toLoggableString( values[i], factory ) ).append( ", " ); @@ -89,36 +86,32 @@ public String toString(Type[] types, Object[] values) throws HibernateException } public String toString(Map namedTypedValues) throws HibernateException { - Map result = new HashMap<>(); + final Map result = new HashMap<>(); for ( Map.Entry entry : namedTypedValues.entrySet() ) { - result.put( - entry.getKey(), entry.getValue().getType().toLoggableString( - entry.getValue().getValue(), - factory - ) - ); + final String key = entry.getKey(); + final TypedValue value = entry.getValue(); + result.put( key, value.getType().toLoggableString( value.getValue(), factory ) ); } return result.toString(); } // Cannot use Map as an argument because it clashes with the previous method (due to type erasure) - public void toString(Iterable> entitiesByEntityKey) throws HibernateException { - if ( !LOG.isDebugEnabled() || !entitiesByEntityKey.iterator().hasNext() ) { - return; - } - - LOG.debug( "Listing entities:" ); - int i = 0; - for ( Map.Entry entityKeyAndEntity : entitiesByEntityKey ) { - final EntityHolder holder = entityKeyAndEntity.getValue(); - if ( holder.getEntity() == null ) { - continue; - } - if ( i++ > 20 ) { - LOG.debug( "More......" ); - break; + public void logEntities(Iterable> entitiesByEntityKey) + throws HibernateException { + if ( LOG.isDebugEnabled() && entitiesByEntityKey.iterator().hasNext() ) { + LOG.debug( "Listing entities:" ); + int i = 0; + for ( Map.Entry entityKeyAndEntity : entitiesByEntityKey ) { + final EntityHolder holder = entityKeyAndEntity.getValue(); + if ( holder.getEntity() == null ) { + continue; + } + if ( i++ > 20 ) { + LOG.debug( "More......" ); + break; + } + LOG.debug( toString( entityKeyAndEntity.getKey().getEntityName(), holder.getEntity() ) ); } - LOG.debug( toString( entityKeyAndEntity.getKey().getEntityName(), holder.getEntity() ) ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EmbeddableCallback.java b/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EmbeddableCallback.java index 6733a6be39a8..7cd4c5b6d127 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EmbeddableCallback.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EmbeddableCallback.java @@ -15,7 +15,12 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry; /** - * Represents a JPA callback on the embeddable type + * Represents a JPA callback method declared by an embeddable type. + * + * @apiNote The JPA specification does not require that we allow + * entity lifecycle callbacks on embeddable classes, and this is + * at least arguably a misfeature. It would by OK to simply remove + * or deprecate this. * * @author Vlad Mihalcea */ @@ -48,18 +53,17 @@ private EmbeddableCallback(Getter embeddableGetter, Method callbackMethod, Callb } @Override - public boolean performCallback(Object entity) { + public void performCallback(Object entity) { try { - Object embeddable = embeddableGetter.get( entity ); + final Object embeddable = embeddableGetter.get( entity ); if ( embeddable != null ) { callbackMethod.invoke( embeddable ); } - return true; } catch (InvocationTargetException e) { //keep runtime exceptions as is - if ( e.getTargetException() instanceof RuntimeException ) { - throw (RuntimeException) e.getTargetException(); + if ( e.getTargetException() instanceof RuntimeException runtimeException ) { + throw runtimeException; } else { throw new RuntimeException( e.getTargetException() ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EntityCallback.java b/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EntityCallback.java index 000eaeb40e21..462339aa96a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EntityCallback.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/EntityCallback.java @@ -15,7 +15,7 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry; /** - * Represents a JPA callback on the entity itself + * Represents a JPA callback method declared by the entity itself. * * @author Kabir Khan * @author Steve Ebersole @@ -50,15 +50,14 @@ private EntityCallback(Method callbackMethod, CallbackType callbackType) { } @Override - public boolean performCallback(Object entity) { + public void performCallback(Object entity) { try { callbackMethod.invoke( entity ); - return true; } catch (InvocationTargetException e) { //keep runtime exceptions as is - if ( e.getTargetException() instanceof RuntimeException ) { - throw (RuntimeException) e.getTargetException(); + if ( e.getTargetException() instanceof RuntimeException runtimeException ) { + throw runtimeException; } else { throw new RuntimeException( e.getTargetException() ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/ListenerCallback.java b/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/ListenerCallback.java index 971452d8452d..c303a3e60bec 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/ListenerCallback.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/event/internal/ListenerCallback.java @@ -15,7 +15,9 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry; /** - * Represents a JPA callback using a dedicated listener + * Represents a JPA callback method declared by an entity listener class. + * + * @see jakarta.persistence.EntityListeners * * @author Kabir Khan * @author Steve Ebersole @@ -49,15 +51,14 @@ public Callback createCallback(ManagedBeanRegistry beanRegistry) { } @Override - public boolean performCallback(Object entity) { + public void performCallback(Object entity) { try { callbackMethod.invoke( listenerManagedBean.getBeanInstance(), entity ); - return true; } catch (InvocationTargetException e) { //keep runtime exceptions as is - if ( e.getTargetException() instanceof RuntimeException ) { - throw (RuntimeException) e.getTargetException(); + if ( e.getTargetException() instanceof RuntimeException runtimeException ) { + throw runtimeException; } else { throw new RuntimeException( e.getTargetException() ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/event/spi/Callback.java b/hibernate-core/src/main/java/org/hibernate/jpa/event/spi/Callback.java index 287a3266df2f..13ace91509ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/event/spi/Callback.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/event/spi/Callback.java @@ -7,11 +7,15 @@ import java.io.Serializable; /** - * Represents a JPA event callback (the method). - * - * Generally there are 2 flavors of this; either an annotated method on the entity itself - * or an annotated method on a separate "listener" class. This contract presents - * a unified abstraction for both cases + * Represents a JPA entity lifecycle callback method. + *

+ * There are two flavors of this, which we abstract here: + *

    + *
  • an annotated method of the entity class itself, or + *
  • an annotated method of a separate entity listener class + * identified via the {@link jakarta.persistence.EntityListeners} + * annotation. + *
* * @author Kabir Khan * @author Steve Ebersole @@ -26,8 +30,6 @@ public interface Callback extends Serializable { * Contract for performing the callback * * @param entity Reference to the entity for which the callback is triggered. - * - * @return Did a callback actually happen? */ - boolean performCallback(Object entity); + void performCallback(Object entity); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java index c7796072c8f3..8bcfeabd44cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java @@ -128,7 +128,7 @@ protected BatchKey getBatchKey() { } public final boolean isModifiableEntity(EntityEntry entry) { - return ( entry == null ? entityPersister().isMutable() : entry.isModifiableEntity() ); + return entry == null ? entityPersister().isMutable() : entry.isModifiableEntity(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java index 6edc24bccbda..122b77b70242 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java @@ -12,7 +12,6 @@ import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; -import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.EntityUniqueKey; import org.hibernate.engine.spi.Mapping; import org.hibernate.engine.spi.PersistenceContext; @@ -25,7 +24,9 @@ import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.engine.internal.ForeignKeys.getEntityIdentifier; import static org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved; +import static org.hibernate.engine.internal.ForeignKeys.isTransient; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; @@ -270,12 +271,12 @@ else if ( y == null ) { // x is not null, but y is, return that x is "greater" return 1; } - - // At this point we know both are non-null. - final Object xId = extractIdentifier( x, factory ); - final Object yId = extractIdentifier( y, factory ); - - return getIdentifierType( factory ).compare( xId, yId ); + else { + // At this point we know both are non-null. + final Object xId = extractIdentifier( x, factory ); + final Object yId = extractIdentifier( y, factory ); + return getIdentifierType( factory ).compare( xId, yId ); + } } private Object extractIdentifier(Object entity, SessionFactoryImplementor factory) { @@ -308,33 +309,32 @@ public Object replace( if ( original == target ) { return target; } - if ( session.getContextEntityIdentifier( original ) == null && - ForeignKeys.isTransient( associatedEntityName, original, Boolean.FALSE, session ) ) { - // original is transient; it is possible that original is a "managed" entity that has - // not been made persistent yet, so check if copyCache contains original as a "managed" value - // that corresponds with some "merge" value. + if ( session.getContextEntityIdentifier( original ) == null + && isTransient( associatedEntityName, original, Boolean.FALSE, session ) ) { + // original is transient; it's possible that original is a "managed" entity + // that has not been made persistent yet, so check if copyCache contains + // original as a "managed" value that corresponds with some "merge" value if ( copyCache.containsValue( original ) ) { return original; } else { // the transient entity is not "managed"; add the merge/managed pair to copyCache - final Object copy = session.getEntityPersister( associatedEntityName, original ) - .instantiate( null, session ); + final Object copy = + session.getEntityPersister( associatedEntityName, original ) + .instantiate( null, session ); copyCache.put( original, copy ); return copy; } } else { - Object id = getIdentifier( original, session ); + final Object id = getIdentifierEvenIfTransient( original, session ); if ( id == null ) { - throw new AssertionFailure( - "non-transient entity has a null id: " + original.getClass() - .getName() - ); + throw new AssertionFailure( "non-transient entity has a null id: " + original.getClass().getName() ); } - id = getIdentifierOrUniqueKeyType( session.getFactory() ) - .replace( id, null, session, owner, copyCache ); - return resolve( id, session, owner ); + final Object replaced = + getIdentifierOrUniqueKeyType( session.getFactory() ) + .replace( id, null, session, owner, copyCache ); + return resolve( replaced, session, owner ); } } } @@ -358,24 +358,26 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) { if ( x == null || y == null ) { return x == y; } - if ( x == y ) { + else if ( x == y ) { return true; } - - final EntityPersister persister = getAssociatedEntityPersister( factory ); - if ( isReferenceToPrimaryKey() ) { - final Object xid = getId( x, persister ); - final Object yid = getId( y, persister ); - // Check for reference equality first as the type-specific checks by IdentifierType are sometimes non-trivial - return xid == yid || persister.getIdentifierType().isEqual( xid, yid, factory ); - } else { - assert uniqueKeyPropertyName != null; - final Type keyType = persister.getPropertyType( uniqueKeyPropertyName ); - final Object xUniqueKey = getUniqueKey( x, keyType, persister ); - final Object yUniqueKey = getUniqueKey( y, keyType, persister ); - return xUniqueKey == yUniqueKey - || keyType.isEqual( xUniqueKey, yUniqueKey, factory ); + final EntityPersister persister = getAssociatedEntityPersister( factory ); + if ( isReferenceToPrimaryKey() ) { + final Object xid = getId( x, persister ); + final Object yid = getId( y, persister ); + // Check for reference equality first as the type-specific + // checks by IdentifierType are sometimes non-trivial + return xid == yid || persister.getIdentifierType().isEqual( xid, yid, factory ); + } + else { + assert uniqueKeyPropertyName != null; + final Type keyType = persister.getPropertyType( uniqueKeyPropertyName ); + final Object xUniqueKey = getUniqueKey( x, keyType, persister ); + final Object yUniqueKey = getUniqueKey( y, keyType, persister ); + return xUniqueKey == yUniqueKey + || keyType.isEqual( xUniqueKey, yUniqueKey, factory ); + } } } @@ -390,15 +392,12 @@ private static Object getId(Object entity, EntityPersister persister) { if ( lazyInitializer != null ) { return lazyInitializer.getInternalIdentifier(); } + else if ( persister.getMappedClass().isInstance( entity ) ) { + return persister.getIdentifier( entity ); + } else { - final Class mappedClass = persister.getMappedClass(); - if ( mappedClass.isInstance( entity ) ) { - return persister.getIdentifier( entity ); - } - else { - //JPA 2 case where @IdClass contains the id and not the associated entity - return entity; - } + //JPA 2 case where @IdClass contains the id and not the associated entity + return entity; } } @@ -408,7 +407,7 @@ private static Object getId(Object entity, EntityPersister persister) { protected Object resolve(Object value, SharedSessionContractImplementor session, Object owner) { if ( value != null && !isNull( owner, session ) ) { if ( isReferenceToPrimaryKey() ) { - return resolveIdentifier( value, session, null ); + return resolveIdentifier( value, session ); } else if ( uniqueKeyPropertyName != null ) { return loadByUniqueKey( getAssociatedEntityName(), uniqueKeyPropertyName, value, session ); @@ -418,18 +417,6 @@ else if ( uniqueKeyPropertyName != null ) { return null; } - /** - * Would an entity be eagerly loaded given the value provided for {@code overridingEager}? - * - * @param overridingEager can override eager from the mapping. - * - * @return If {@code overridingEager} is null, then it does not override. - * If true or false then it overrides the mapping value. - */ - public boolean isEager(Boolean overridingEager) { - return overridingEager != null ? overridingEager : this.eager; - } - public EntityPersister getAssociatedEntityPersister(final SessionFactoryImplementor factory) { final EntityPersister persister = associatedEntityPersister; //The following branch implements a simple lazy-initialization, but rather than the canonical @@ -446,6 +433,19 @@ public EntityPersister getAssociatedEntityPersister(final SessionFactoryImplemen } } + protected final Object getIdentifierEvenIfTransient(Object value, SharedSessionContractImplementor session) + throws HibernateException { + if ( isReferenceToIdentifierProperty() ) { + return getEntityIdentifier( getAssociatedEntityName(), value, session ); //tolerates nulls and transients + } + else if ( value == null ) { + return null; + } + else { + return getUniqueKey( value, session ); + } + } + protected final Object getIdentifier(Object value, SharedSessionContractImplementor session) throws HibernateException { if ( isReferenceToIdentifierProperty() ) { @@ -455,33 +455,37 @@ else if ( value == null ) { return null; } else { - final LazyInitializer lazyInitializer = extractLazyInitializer( value ); - if ( lazyInitializer != null ) { - // If the value is a Proxy and the property access is field, the value returned by - // attributeMapping.getAttributeMetadata().getPropertyAccess().getGetter().get( object ) - // is always null except for the id, we need the to use the proxy implementation to - // extract the property value. - value = lazyInitializer.getImplementation(); - } - else if ( isPersistentAttributeInterceptable( value ) ) { - // If the value is an instance of PersistentAttributeInterceptable, and it is - // not initialized, we need to force initialization the get the property value - final PersistentAttributeInterceptor interceptor = - asPersistentAttributeInterceptable( value ).$$_hibernate_getInterceptor(); - if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { - lazinessInterceptor.forceInitialize( value, null ); - } + return getUniqueKey( value, session ); + } + } + + private Object getUniqueKey(Object value, SharedSessionContractImplementor session) { + final LazyInitializer lazyInitializer = extractLazyInitializer( value ); + if ( lazyInitializer != null ) { + // If the value is a Proxy and the property access is field, the value returned by + // attributeMapping.getAttributeMetadata().getPropertyAccess().getGetter().get( object ) + // is always null except for the id, we need the to use the proxy implementation to + // extract the property value. + value = lazyInitializer.getImplementation(); + } + else if ( isPersistentAttributeInterceptable( value ) ) { + // If the value is an instance of PersistentAttributeInterceptable, and it is + // not initialized, we need to force initialization the get the property value + final PersistentAttributeInterceptor interceptor = + asPersistentAttributeInterceptable( value ).$$_hibernate_getInterceptor(); + if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { + lazinessInterceptor.forceInitialize( value, null ); } - final EntityPersister entityPersister = getAssociatedEntityPersister( session.getFactory() ); - final Object propertyValue = entityPersister.getPropertyValue( value, uniqueKeyPropertyName ); - // We now have the value of the property-ref we reference. However, - // we need to dig a little deeper, as that property might also be - // an entity type, in which case we need to resolve its identifier - final Type type = entityPersister.getPropertyType( uniqueKeyPropertyName ); - return type instanceof EntityType entityType - ? entityType.getIdentifier( propertyValue, session ) - : propertyValue; } + final EntityPersister entityPersister = getAssociatedEntityPersister( session.getFactory() ); + final Object propertyValue = entityPersister.getPropertyValue( value, uniqueKeyPropertyName ); + // We now have the value of the property-ref we reference. However, + // we need to dig a little deeper, as that property might also be + // an entity type, in which case we need to resolve its identifier + final Type type = entityPersister.getPropertyType( uniqueKeyPropertyName ); + return type instanceof EntityType entityType + ? entityType.getIdentifierEvenIfTransient( propertyValue, session ) + : propertyValue; } protected final Object getIdentifier(Object value, SessionFactoryImplementor sessionFactory) @@ -709,18 +713,17 @@ public boolean isReferenceToIdentifierProperty() { * * @throws HibernateException Indicates problems performing the load. */ - protected final Object resolveIdentifier(Object id, SharedSessionContractImplementor session, Boolean overridingEager) + protected final Object resolveIdentifier(Object id, SharedSessionContractImplementor session) throws HibernateException { - final boolean isEager = isEager( overridingEager ); // If the association is lazy, retrieve the concrete type if required - final String entityName = isEager + final String entityName = eager ? getAssociatedEntityName() : getAssociatedEntityPersister( session.getFactory() ) .resolveConcreteProxyTypeForId( id, session ) .getEntityName(); - final Object proxyOrEntity = session.internalLoad( entityName, id, isEager, isNullable() ); + final Object proxyOrEntity = session.internalLoad( entityName, id, eager, isNullable() ); final LazyInitializer lazyInitializer = extractLazyInitializer( proxyOrEntity ); if ( lazyInitializer != null ) { @@ -732,10 +735,6 @@ protected final Object resolveIdentifier(Object id, SharedSessionContractImpleme return proxyOrEntity; } - protected final Object resolveIdentifier(Object id, SharedSessionContractImplementor session) throws HibernateException { - return resolveIdentifier( id, session, null ); - } - protected boolean isNull(Object owner, SharedSessionContractImplementor session) { return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java index e5f9fa1896e2..e9aac6f07629 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java @@ -155,7 +155,7 @@ else if ( old == null ) { else { // the ids are fully resolved, so compare them with isDirty(), not isModified() return getIdentifierOrUniqueKeyType( session.getFactory() ) - .isDirty( old, getIdentifier( current, session ), session ); + .isDirty( old, getIdentifierEvenIfTransient( current, session ), session ); } } @@ -240,8 +240,8 @@ public boolean isDirty( return false; } else { - final Object oldid = getIdentifier( old, session ); - final Object newid = getIdentifier( current, session ); + final Object oldid = getIdentifierEvenIfTransient( old, session ); + final Object newid = getIdentifierEvenIfTransient( current, session ); return getIdentifierOrUniqueKeyType( session.getFactory() ).isDirty( oldid, newid, session ); } } @@ -259,8 +259,8 @@ else if ( isSame( old, current ) ) { return false; } else { - final Object oldid = getIdentifier( old, session ); - final Object newid = getIdentifier( current, session ); + final Object oldid = getIdentifierEvenIfTransient( old, session ); + final Object newid = getIdentifierEvenIfTransient( current, session ); return getIdentifierOrUniqueKeyType( session.getFactory() ).isDirty( oldid, newid, checkable, session ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/Type.java b/hibernate-core/src/main/java/org/hibernate/type/Type.java index bc4efd237820..8800c70799aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/Type.java +++ b/hibernate-core/src/main/java/org/hibernate/type/Type.java @@ -226,7 +226,8 @@ default String getReturnedClassName() { * * @throws HibernateException A problem occurred performing the comparison */ - boolean isEqual(@Nullable Object x, @Nullable Object y, SessionFactoryImplementor factory) throws HibernateException; + boolean isEqual(@Nullable Object x, @Nullable Object y, SessionFactoryImplementor factory) + throws HibernateException; /** * Get a hash code, consistent with persistence "equality". For most types this could @@ -286,7 +287,8 @@ default String getReturnedClassName() { * * @throws HibernateException A problem occurred performing the checking */ - boolean isDirty(@Nullable Object old, @Nullable Object current, SharedSessionContractImplementor session) throws HibernateException; + boolean isDirty(@Nullable Object old, @Nullable Object current, SharedSessionContractImplementor session) + throws HibernateException; /** * Should the parent be considered dirty, given both the old and current value? @@ -323,7 +325,7 @@ boolean isModified( @Nullable Object currentState, boolean[] checkable, SharedSessionContractImplementor session) - throws HibernateException; + throws HibernateException; /** * Bind a value represented by an instance of the {@link #getReturnedClass() mapped class} @@ -346,7 +348,7 @@ void nullSafeSet( int index, boolean[] settable, SharedSessionContractImplementor session) - throws HibernateException, SQLException; + throws HibernateException, SQLException; /** * Bind a value represented by an instance of the {@link #getReturnedClass() mapped class} @@ -363,7 +365,7 @@ void nullSafeSet( * @throws SQLException An error from the JDBC driver */ void nullSafeSet(PreparedStatement st, @Nullable Object value, int index, SharedSessionContractImplementor session) - throws HibernateException, SQLException; + throws HibernateException, SQLException; /** * Generate a representation of the given value for logging purposes. @@ -376,7 +378,7 @@ void nullSafeSet(PreparedStatement st, @Nullable Object value, int index, Shared * @throws HibernateException An error from Hibernate */ String toLoggableString(@Nullable Object value, SessionFactoryImplementor factory) - throws HibernateException; + throws HibernateException; /** * Returns the abbreviated name of the type. @@ -434,7 +436,8 @@ String toLoggableString(@Nullable Object value, SessionFactoryImplementor factor * * @throws HibernateException An error from Hibernate */ - default @Nullable Serializable disassemble(@Nullable Object value, SessionFactoryImplementor sessionFactory) throws HibernateException { + default @Nullable Serializable disassemble(@Nullable Object value, SessionFactoryImplementor sessionFactory) + throws HibernateException { return disassemble( value, null, null ); } @@ -452,7 +455,8 @@ String toLoggableString(@Nullable Object value, SessionFactoryImplementor factor * * @throws HibernateException An error from Hibernate */ - @Nullable Serializable disassemble(@Nullable Object value, @Nullable SharedSessionContractImplementor session, @Nullable Object owner) throws HibernateException; + @Nullable Serializable disassemble(@Nullable Object value, @Nullable SharedSessionContractImplementor session, @Nullable Object owner) + throws HibernateException; /** * Reconstruct the object from its disassembled state. This function is the inverse of @@ -466,7 +470,8 @@ String toLoggableString(@Nullable Object value, SessionFactoryImplementor factor * * @throws HibernateException An error from Hibernate */ - @Nullable Object assemble(@Nullable Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException; + @Nullable Object assemble(@Nullable Serializable cached, SharedSessionContractImplementor session, Object owner) + throws HibernateException; /** * Called before assembling a query result set from the query cache, to allow batch diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/isdirty/IsDirtyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/isdirty/IsDirtyTest.java new file mode 100644 index 000000000000..e735ac56ab0c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/isdirty/IsDirtyTest.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.isdirty; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SessionFactory +@DomainModel(annotatedClasses = {IsDirtyTest.X.class, IsDirtyTest.Y.class}) +public class IsDirtyTest { + @Test void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertFalse( session.isDirty() ); + X x = new X(); + session.persist( x ); + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + x.i = 2; + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + session.remove( x ); + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + } ); + scope.inTransaction( session -> { + assertFalse( session.isDirty() ); + X x = new X(); + Y y = new Y(); + List ys = x.ys; + x.ys.add( y ); + y.x = x; + session.persist( x ); + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + y.x = null; + assertTrue( session.isDirty() ); + y.x = x; + assertFalse( session.isDirty() ); + x.ys = null; + assertTrue( session.isDirty() ); + x.ys = ys; + assertFalse( session.isDirty() ); + ys.clear(); + assertTrue( session.isDirty() ); + ys.add( y ); + assertFalse( session.isDirty() ); + x.strings.add( "hello" ); + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + x.strings.add( "world" ); + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + x.strings.clear(); + assertTrue( session.isDirty() ); + session.flush(); + assertFalse( session.isDirty() ); + y.x = new X(); + assertTrue( session.isDirty() ); + y.x = x; + assertFalse( session.isDirty() ); + ys.add( new Y() ); + assertTrue( session.isDirty() ); + } ); + } + @Entity + static class X { + @Id @GeneratedValue + Long id; + int i = 1; + @OneToMany(cascade = CascadeType.PERSIST) + List ys = new ArrayList<>(); + @ElementCollection + Set strings = new HashSet<>(); + } + @Entity + static class Y { + @Id @GeneratedValue + Long id; + @ManyToOne(fetch = FetchType.LAZY) + X x; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/isdirty/SessionIsDirtyForNewManyToOneObjectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/isdirty/SessionIsDirtyForNewManyToOneObjectTest.java new file mode 100644 index 000000000000..7a1efdd7ea75 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/isdirty/SessionIsDirtyForNewManyToOneObjectTest.java @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.isdirty; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import org.hibernate.Session; +import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SessionIsDirtyForNewManyToOneObjectTest extends BaseSessionFactoryFunctionalTest { + private Long parentId; + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { EntityChild.class, EntityChildAssigned.class, EntityParent.class }; + } + @BeforeEach + public void setUp() { + inTransaction( session -> { + EntityChild child = new EntityChild("InitialChild"); + EntityChildAssigned assigned = new EntityChildAssigned(1L, "InitialChild"); + EntityParent parent = new EntityParent("InitialParent", child, assigned); + session.persist(child); + session.persist(parent); + session.persist(assigned); + session.flush(); + parentId = parent.id; + } ); + } + @Test + public void SessionIsDirtyShouldNotFailForNewManyToOneObject() + { + inTransaction( session -> { + var parent = getParent(session); + EntityChild nextChild = new EntityChild("NewManyToOneChild"); + //parent.Child entity is not cascaded, I want to save it explicitly later + parent.child = nextChild; + // will throw TransientObjectException + assertDoesNotThrow(()->session.isDirty(), "session.isDirty() call should not fail for transient many-to-one object referenced in session.\""); + assertTrue(session.isDirty(),"session.isDirty() call should return true."); + session.persist(nextChild); + }); + } + @Test + public void SessionIsDirtyShouldNotFailForNewManyToOneObjectWithAssignedId() + { + inTransaction( session -> { + var parent = getParent(session); + EntityChildAssigned nextChildAssigned = new EntityChildAssigned(2L, "NewManyToOneChildAssignedId"); + //parent.ChildAssigned entity is not cascaded, I want to save it explicitly later + parent.childAssigned = nextChildAssigned; + assertDoesNotThrow(()->session.isDirty(), "session.isDirty() call should not fail for transient many-to-one object referenced in session.\""); + assertTrue(session.isDirty(),"session.isDirty() call should return true."); + session.persist(nextChildAssigned); + }); + } + private EntityParent getParent(Session session){ + return session.get(EntityParent.class, parentId); + } + @AfterEach + public void cleanUp() { + inTransaction( + session -> { + session.createQuery("delete from EntityParent").executeUpdate(); + session.createQuery("delete from EntityChild").executeUpdate(); + session.createQuery("delete from EntityChildAssigned").executeUpdate(); + } + ); + } + @Entity(name = "EntityChild") + public static class EntityChild + { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + Long id; + String name; + EntityChild(){} + EntityChild(String name){this.name = name;} + } + @Entity(name = "EntityParent") + public static class EntityParent + { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + Long id; + String name; + @ManyToOne + EntityChild child; + @ManyToOne + EntityChildAssigned childAssigned; + EntityParent(){} + EntityParent(String name, EntityChild child, EntityChildAssigned childAssigned){ + this.name = name; + this.child = child; + this.childAssigned = childAssigned; + } + } + @Entity(name = "EntityChildAssigned") + public static class EntityChildAssigned + { + @Id + Long id; + String name; + EntityChildAssigned(){} + EntityChildAssigned(Long id, String name){ + this.id = id; + this.name = name; + } + } +}