diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java index c3b445ee11e7..441663234872 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java @@ -18,10 +18,11 @@ import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.pretty.MessageHelper; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import static org.hibernate.pretty.MessageHelper.infoString; + /** * Defines the default evict event listener used by hibernate for evicting entities * in response to generated flush events. In particular, this implementation will @@ -53,23 +54,23 @@ public void onEvict(EvictEvent event) throws HibernateException { if ( id == null ) { throw new IllegalArgumentException( "Could not determine identifier of proxy passed to evict()" ); } - final EntityPersister persister = source.getFactory() - .getMappingMetamodel() - .getEntityDescriptor( lazyInitializer.getEntityName() ); + final EntityPersister persister = + source.getFactory().getMappingMetamodel() + .getEntityDescriptor( lazyInitializer.getEntityName() ); final EntityKey key = source.generateEntityKey( id, persister ); final EntityHolder holder = persistenceContext.detachEntity( key ); // if the entity has been evicted then its holder is null if ( holder != null && !lazyInitializer.isUninitialized() ) { final Object entity = holder.getEntity(); if ( entity != null ) { - EntityEntry entry = persistenceContext.removeEntry( entity ); + final EntityEntry entry = persistenceContext.removeEntry( entity ); doEvict( entity, key, entry.getPersister(), event.getSession() ); } } lazyInitializer.unsetSession(); } else { - EntityEntry entry = persistenceContext.getEntry( object ); + final EntityEntry entry = persistenceContext.getEntry( object ); if ( entry != null ) { doEvict( object, entry.getEntityKey(), entry.getPersister(), source ); } @@ -85,11 +86,12 @@ public void onEvict(EvictEvent event) throws HibernateException { * requires with EntityManager.detach(). */ private static void checkEntity(Object object, EventSource source) { - String entityName = source.getSession().guessEntityName( object ); + final String entityName = source.getSession().guessEntityName( object ); if ( entityName != null ) { try { - EntityPersister persister = source.getFactory().getMappingMetamodel() - .getEntityDescriptor( entityName ); + final EntityPersister persister = + source.getFactory().getMappingMetamodel() + .getEntityDescriptor( entityName ); if ( persister != null ) { return; //ALL GOOD } @@ -107,7 +109,7 @@ protected void doEvict( final EventSource session) throws HibernateException { if ( LOG.isTraceEnabled() ) { - LOG.tracev( "Evicting {0}", MessageHelper.infoString( persister ) ); + LOG.tracev( "Evicting {0}", infoString( persister ) ); } final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); 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 8a80bae75755..c613d649669e 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 @@ -35,7 +35,6 @@ import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.pretty.MessageHelper; import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.Type; @@ -50,6 +49,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.pretty.MessageHelper.infoString; /** * An event that occurs for each entity instance at flush time @@ -67,9 +67,9 @@ public void injectCallbackRegistry(CallbackRegistry callbackRegistry) { } /** - * make sure user didn't mangle the id + * Make sure user didn't mangle the id. */ - public void checkId(Object object, EntityPersister persister, Object id, SessionImplementor session) + public void checkId(Object object, EntityPersister persister, Object id, Status status, SessionImplementor session) throws HibernateException { if ( id instanceof DelayedPostInsertIdentifier ) { @@ -79,22 +79,17 @@ public void checkId(Object object, EntityPersister persister, Object id, Session } final Object oid = persister.getIdentifier( object, session ); - if ( id == null ) { throw new AssertionFailure( "null id in " + persister.getEntityName() + " entry (don't flush the Session after an exception occurs)" ); } - - //Small optimisation: always try to avoid getIdentifierType().isEqual(..) when possible. - //(However it's not safe to invoke the equals() method as it might trigger side-effects) - if ( id == oid ) { - //No further checks necessary: - return; - } - - if ( !persister.getIdentifierType().isEqual( id, oid, session.getFactory() ) ) { + // Small optimisation: always try to avoid getIdentifierType().isEqual(..) when possible. + // (However it's not safe to invoke the equals() method as it might trigger side effects.) + else if ( id != oid + && !status.isDeletedOrGone() + && !persister.getIdentifierType().isEqual( id, oid, session.getFactory() ) ) { throw new HibernateException( "identifier of an instance of " + persister.getEntityName() - + " was altered from " + oid + " to " + id ); + + " was altered from " + oid + " to " + id ); } } @@ -115,7 +110,8 @@ private void checkNaturalId( private static boolean isUninitializedEnhanced(Object entity) { if ( isPersistentAttributeInterceptable( entity ) ) { - final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + final PersistentAttributeInterceptor interceptor = + asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); // the entity is an un-initialized enhancement-as-proxy reference return interceptor instanceof EnhancementAsProxyLazinessInterceptor; } @@ -167,7 +163,6 @@ 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(); @@ -177,9 +172,9 @@ else if ( !mightBeDirty && loadedState != null ) { } else { final EntityPersister persister = entry.getPersister(); - checkId( entity, persister, entry.getId(), session ); + checkId( entity, persister, entry.getId(), entry.getStatus(), session ); // grab its current state - Object[] values = persister.getValues( entity ); + final Object[] values = persister.getValues( entity ); checkNaturalId( persister, entity, entry, values, loadedState, session ); return values; } @@ -221,8 +216,7 @@ private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mi final Object entity = event.getEntity(); processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes ); processIfManagedEntity( entity, DefaultFlushEntityEventListener::useTracker ); - event.getFactory() - .getCustomEntityDirtinessStrategy() + event.getFactory().getCustomEntityDirtinessStrategy() .resetDirty( entity, entry.getPersister(), event.getSession() ); return false; } @@ -251,7 +245,7 @@ private boolean scheduleUpdate(final FlushEntityEvent event) { // increment the version number (if necessary) final Object nextVersion = getNextVersion( event ); - int[] dirtyProperties = getDirtyProperties( event, intercepted ); + final int[] dirtyProperties = getDirtyProperties( event, intercepted ); // check nullability but do not doAfterTransactionCompletion command execute // we'll use scheduled updates for that. @@ -281,7 +275,7 @@ private boolean scheduleUpdate(final FlushEntityEvent event) { } private static int[] getDirtyProperties(FlushEntityEvent event, boolean intercepted) { - int[] dirtyProperties = event.getDirtyProperties(); + final int[] dirtyProperties = event.getDirtyProperties(); if ( event.isDirtyCheckPossible() && dirtyProperties == null ) { if ( !intercepted && !event.hasDirtyCollection() ) { throw new AssertionFailure( "dirty, but no dirty properties" ); @@ -302,26 +296,26 @@ private static void logScheduleUpdate(EntityEntry entry, SessionFactoryImplement if ( !persister.isMutable() ) { LOG.tracev( "Updating immutable, deleted entity: {0}", - MessageHelper.infoString(persister, entry.getId(), factory) + infoString(persister, entry.getId(), factory) ); } else if ( !entry.isModifiableEntity() ) { LOG.tracev( "Updating non-modifiable, deleted entity: {0}", - MessageHelper.infoString(persister, entry.getId(), factory) + infoString(persister, entry.getId(), factory) ); } else { LOG.tracev( "Updating deleted entity: {0}", - MessageHelper.infoString(persister, entry.getId(), factory) + infoString(persister, entry.getId(), factory) ); } } else { LOG.tracev( "Updating entity: {0}", - MessageHelper.infoString(persister, entry.getId(), factory) + infoString(persister, entry.getId(), factory) ); } } @@ -345,12 +339,12 @@ protected boolean invokeInterceptor(FlushEntityEvent event) { final EntityPersister persister = entry.getPersister(); final EventSource session = event.getSession(); - boolean isDirty = false; - - if ( entry.getStatus() != Status.DELETED ) { - if ( callbackRegistry.preUpdate( entity ) ) { - isDirty = copyState( entity, persister.getPropertyTypes(), values, event.getFactory() ); - } + final boolean isDirty; + if ( entry.getStatus() != Status.DELETED && callbackRegistry.preUpdate( entity ) ) { + isDirty = copyState( entity, persister.getPropertyTypes(), values, event.getFactory() ); + } + else { + isDirty = false; } final boolean stateModified = session.getInterceptor().onFlushDirty( @@ -419,7 +413,7 @@ private static boolean isVersionIncrementRequired(FlushEntityEvent event, Entity return false; } else { - int[] dirtyProperties = event.getDirtyProperties(); + final int[] dirtyProperties = event.getDirtyProperties(); return dirtyProperties == null || Versioning.isVersionIncrementRequired( dirtyProperties, @@ -450,7 +444,7 @@ private boolean hasDirtyCollections(FlushEntityEvent event) { persister.getPropertyVersionability() ); visitor.processEntityPropertyValues( event.getPropertyValues(), persister.getPropertyTypes() ); - boolean hasDirtyCollections = visitor.wasDirtyCollectionFound(); + final boolean hasDirtyCollections = visitor.wasDirtyCollectionFound(); event.setHasDirtyCollection( hasDirtyCollections ); return hasDirtyCollections; } @@ -487,7 +481,7 @@ protected void dirtyCheck(final FlushEntityEvent event) throws HibernateExceptio private static int[] performDirtyCheck(FlushEntityEvent event) { final SessionImplementor session = event.getSession(); - boolean dirtyCheckPossible; + final boolean dirtyCheckPossible; int[] dirtyProperties = null; final EventManager eventManager = session.getEventManager(); final HibernateMonitoringEvent dirtyCalculationEvent = eventManager.beginDirtyCalculationEvent(); @@ -552,16 +546,15 @@ else if ( entry.getStatus() == Status.DELETED && !entry.isModifiableEntity() ) { * the bytecode enhancement, or a custom dirtiness strategy. */ private static int[] getDirtyProperties(FlushEntityEvent event) { - int[] dirtyProperties = getDirtyPropertiesFromInterceptor( event ); + final int[] dirtyProperties = getDirtyPropertiesFromInterceptor( event ); if ( dirtyProperties != null ) { return dirtyProperties; } else { final Object entity = event.getEntity(); - if ( isSelfDirtinessTracker( entity ) && asManagedEntity( entity ).$$_hibernate_useTracker() ) { - return getDirtyPropertiesFromSelfDirtinessTracker( asSelfDirtinessTracker( entity ), event ); - } - return getDirtyPropertiesFromCustomEntityDirtinessStrategy( event ); + return isSelfDirtinessTracker( entity ) && asManagedEntity( entity ).$$_hibernate_useTracker() + ? getDirtyPropertiesFromSelfDirtinessTracker( asSelfDirtinessTracker( entity ), event ) + : getDirtyPropertiesFromCustomEntityDirtinessStrategy( event ); } } @@ -690,7 +683,7 @@ private void logDirtyProperties(EntityEntry entry, int[] dirtyProperties) { } LOG.tracev( "Found dirty properties [{0}] : {1}", - MessageHelper.infoString( persister.getEntityName(), entry.getId() ), + infoString( persister.getEntityName(), entry.getId() ), Arrays.toString( dirtyPropertyNames ) ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/ReSaveReferencedDeletedEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/ReSaveReferencedDeletedEntity.java new file mode 100644 index 000000000000..855055d7cb2d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/ReSaveReferencedDeletedEntity.java @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; + +public class ReSaveReferencedDeletedEntity extends BaseCoreFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Child.class, Parent.class }; + } + + @Override + protected void configure(Configuration configuration) { + super.configure( configuration ); + configuration.setProperty( AvailableSettings.USE_IDENTIFIER_ROLLBACK, "true" ); + } + + @Test + @JiraKey("HHH-14416") + public void testReSaveDeletedEntity() { + doInHibernate( this::sessionFactory, session -> { + Parent parent = new Parent(); + + Child child = new Child(); + parent.setChild( child ); + + session.persist( parent ); + + parent.setChild( null ); + session.remove(child); + + session.flush(); + + parent.setChild( child ); + session.persist(child); + } ); + } + + @Test + @JiraKey("HHH-14416") + public void testReSaveDeletedEntityWithDetach() { + doInHibernate( this::sessionFactory, session -> { + Parent parent = new Parent(); + + Child child = new Child(); + parent.setChild( child ); + + session.persist( parent ); + + parent.setChild( null ); + session.remove(child); + + session.flush(); + session.detach(child); + + parent.setChild( child ); + session.persist(child); + } ); + } + + @Entity(name = "Child") + public static class Child { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @Entity(name = "Parent") + public static class Parent { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @OneToOne(cascade = CascadeType.ALL) + private Child child; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Child getChild() { + return child; + } + + public void setChild(Child child) { + this.child = child; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/ReSaveReferencedDeletedEntityJPA.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/ReSaveReferencedDeletedEntityJPA.java new file mode 100644 index 000000000000..5de83db03f76 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/ReSaveReferencedDeletedEntityJPA.java @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.Map; + +public class ReSaveReferencedDeletedEntityJPA extends BaseEntityManagerFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Child.class, Parent.class }; + } + + @Override + protected Map buildSettings() { + Map settings = super.buildSettings(); + settings.put( AvailableSettings.USE_IDENTIFIER_ROLLBACK, "true" ); + return settings; + } + + @Test + @ JiraKey("HHH-14416") + public void testRefreshUnDeletedEntityWithReferencesJPA() { + EntityManager em = getOrCreateEntityManager(); + em.getTransaction().begin(); + + Parent parent = new Parent(); + parent.setId(1); + + Child child = new Child(); + child.setId(2); + parent.setChild( child ); + + em.persist( parent ); + + em.flush(); + + em.remove( parent ); + + em.flush(); + + em.detach( parent ); + + em.persist( parent ); + + em.flush(); + + em.refresh( child ); + + em.getTransaction().commit(); + } + + @Test + @JiraKey("HHH-14416") + public void testReSaveDeletedEntityWithReferencesJPA() { + EntityManager em = getOrCreateEntityManager(); + em.getTransaction().begin(); + + Parent parent = new Parent(); + parent.setId(1); + + Child child = new Child(); + child.setId(2); + parent.setChild( child ); + + em.persist( parent ); + + parent.setChild( null ); + em.remove( child ); + + em.persist( child ); + + em.getTransaction().commit(); + } + + @Entity(name = "Child") + public static class Child { + @Id + private Integer id; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Integer id; + + @OneToOne(cascade = CascadeType.ALL) + private Child child; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Child getChild() { + return child; + } + + public void setChild(Child child) { + this.child = child; + } + } +}