HHH-16690 Fix re-saving for unloaded deletes
This commit is contained in:
parent
b6733c413d
commit
ecbcc2d940
|
@ -1867,6 +1867,17 @@ public class StatefulPersistenceContext implements PersistenceContext {
|
||||||
deletedUnloadedEntityKeys.add( key );
|
deletedUnloadedEntityKeys.add( key );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeDeletedUnloadedEntityKey(EntityKey key) {
|
||||||
|
assert deletedUnloadedEntityKeys != null;
|
||||||
|
deletedUnloadedEntityKeys.remove( key );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsDeletedUnloadedEntityKeys() {
|
||||||
|
return deletedUnloadedEntityKeys != null && !deletedUnloadedEntityKeys.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCollectionEntriesSize() {
|
public int getCollectionEntriesSize() {
|
||||||
return collectionEntries == null ? 0 : collectionEntries.size();
|
return collectionEntries == null ? 0 : collectionEntries.size();
|
||||||
|
|
|
@ -50,6 +50,7 @@ import org.hibernate.internal.util.StringHelper;
|
||||||
import org.hibernate.internal.util.collections.CollectionHelper;
|
import org.hibernate.internal.util.collections.CollectionHelper;
|
||||||
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.metamodel.mapping.internal.EntityCollectionPart;
|
import org.hibernate.metamodel.mapping.internal.EntityCollectionPart;
|
||||||
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
import org.hibernate.proxy.HibernateProxy;
|
import org.hibernate.proxy.HibernateProxy;
|
||||||
import org.hibernate.proxy.LazyInitializer;
|
import org.hibernate.proxy.LazyInitializer;
|
||||||
import org.hibernate.type.CollectionType;
|
import org.hibernate.type.CollectionType;
|
||||||
|
@ -846,6 +847,26 @@ public class ActionQueue {
|
||||||
|| ( collectionCreations != null && !collectionCreations.isEmpty() );
|
|| ( collectionCreations != null && !collectionCreations.isEmpty() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void unScheduleUnloadedDeletion(Object newEntity) {
|
||||||
|
final EntityPersister entityPersister = session.getEntityPersister( null, newEntity );
|
||||||
|
final Object identifier = entityPersister.getIdentifier( newEntity, session );
|
||||||
|
if ( deletions != null ) {
|
||||||
|
for ( int i = 0; i < deletions.size(); i++ ) {
|
||||||
|
EntityDeleteAction action = deletions.get( i );
|
||||||
|
if ( action.getInstance() == null
|
||||||
|
&& action.getEntityName().equals( entityPersister.getEntityName() )
|
||||||
|
&& entityPersister.getIdentifierMapping().areEqual( action.getId(), identifier, session ) ) {
|
||||||
|
session.getPersistenceContextInternal().removeDeletedUnloadedEntityKey(
|
||||||
|
session.generateEntityKey( identifier, entityPersister )
|
||||||
|
);
|
||||||
|
deletions.remove( i );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionFailure( "Unable to perform un-delete for unloaded entity delete " + entityPersister.getEntityName() );
|
||||||
|
}
|
||||||
|
|
||||||
public void unScheduleDeletion(EntityEntry entry, Object rescuedEntity) {
|
public void unScheduleDeletion(EntityEntry entry, Object rescuedEntity) {
|
||||||
final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( rescuedEntity );
|
final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( rescuedEntity );
|
||||||
if ( lazyInitializer != null ) {
|
if ( lazyInitializer != null ) {
|
||||||
|
|
|
@ -757,6 +757,10 @@ public interface PersistenceContext {
|
||||||
|
|
||||||
void registerDeletedUnloadedEntityKey(EntityKey key);
|
void registerDeletedUnloadedEntityKey(EntityKey key);
|
||||||
|
|
||||||
|
void removeDeletedUnloadedEntityKey(EntityKey key);
|
||||||
|
|
||||||
|
boolean containsDeletedUnloadedEntityKeys();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of the internal map storing all collection entries.
|
* The size of the internal map storing all collection entries.
|
||||||
* (The map is not exposed directly, but the size is often useful)
|
* (The map is not exposed directly, but the size is often useful)
|
||||||
|
|
|
@ -1141,6 +1141,11 @@ public class SessionDelegatorBaseImpl implements SessionImplementor {
|
||||||
delegate.forceFlush( e );
|
delegate.forceFlush( e );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void forceFlush(EntityKey e) throws HibernateException {
|
||||||
|
delegate.forceFlush( e );
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void merge(String entityName, Object object, MergeContext copiedAlready) throws HibernateException {
|
public void merge(String entityName, Object object, MergeContext copiedAlready) throws HibernateException {
|
||||||
delegate.merge( entityName, object, copiedAlready );
|
delegate.merge( entityName, object, copiedAlready );
|
||||||
|
|
|
@ -87,6 +87,10 @@ public interface SessionImplementor extends Session, SharedSessionContractImplem
|
||||||
* Initiate a flush to force deletion of a re-persisted entity.
|
* Initiate a flush to force deletion of a re-persisted entity.
|
||||||
*/
|
*/
|
||||||
void forceFlush(EntityEntry e) throws HibernateException;
|
void forceFlush(EntityEntry e) throws HibernateException;
|
||||||
|
/**
|
||||||
|
* Initiate a flush to force deletion of a re-persisted entity.
|
||||||
|
*/
|
||||||
|
void forceFlush(EntityKey e) throws HibernateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cascade the lock operation to the given child entity.
|
* Cascade the lock operation to the given child entity.
|
||||||
|
|
|
@ -203,6 +203,9 @@ public abstract class AbstractSaveEventListener<C>
|
||||||
throw new NonUniqueObjectException( id, persister.getEntityName() );
|
throw new NonUniqueObjectException( id, persister.getEntityName() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if ( persistenceContext.containsDeletedUnloadedEntityKey( key ) ) {
|
||||||
|
source.forceFlush( key );
|
||||||
|
}
|
||||||
persister.setIdentifier( entity, id, source );
|
persister.setIdentifier( entity, id, source );
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,6 +156,19 @@ public class DefaultMergeEventListener
|
||||||
entityIsPersistent( event, copiedAlready );
|
entityIsPersistent( event, copiedAlready );
|
||||||
break;
|
break;
|
||||||
default: //DELETED
|
default: //DELETED
|
||||||
|
if ( event.getSession().getPersistenceContext().getEntry( entity ) == null ) {
|
||||||
|
assert event.getSession().getPersistenceContext().containsDeletedUnloadedEntityKey(
|
||||||
|
event.getSession().generateEntityKey(
|
||||||
|
event.getSession()
|
||||||
|
.getEntityPersister( event.getEntityName(), entity )
|
||||||
|
.getIdentifier( entity, event.getSession() ),
|
||||||
|
event.getSession().getEntityPersister( event.getEntityName(), entity )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
event.getSession().getActionQueue().unScheduleUnloadedDeletion( entity );
|
||||||
|
entityIsDetached(event, copiedAlready);
|
||||||
|
break;
|
||||||
|
}
|
||||||
throw new ObjectDeletedException(
|
throw new ObjectDeletedException(
|
||||||
"deleted instance passed to merge",
|
"deleted instance passed to merge",
|
||||||
null,
|
null,
|
||||||
|
@ -174,7 +187,8 @@ public class DefaultMergeEventListener
|
||||||
EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
|
EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
|
||||||
Object id = persister.getIdentifier( entity, source );
|
Object id = persister.getIdentifier( entity, source );
|
||||||
if ( id != null ) {
|
if ( id != null ) {
|
||||||
final Object managedEntity = persistenceContext.getEntity( source.generateEntityKey( id, persister ) );
|
final EntityKey entityKey = source.generateEntityKey( id, persister );
|
||||||
|
final Object managedEntity = persistenceContext.getEntity( entityKey );
|
||||||
entry = persistenceContext.getEntry( managedEntity );
|
entry = persistenceContext.getEntry( managedEntity );
|
||||||
if ( entry != null ) {
|
if ( entry != null ) {
|
||||||
// we have a special case of a detached entity from the
|
// we have a special case of a detached entity from the
|
||||||
|
|
|
@ -8,10 +8,13 @@ package org.hibernate.event.internal;
|
||||||
|
|
||||||
import org.hibernate.engine.internal.ForeignKeys;
|
import org.hibernate.engine.internal.ForeignKeys;
|
||||||
import org.hibernate.engine.spi.EntityEntry;
|
import org.hibernate.engine.spi.EntityEntry;
|
||||||
|
import org.hibernate.engine.spi.EntityKey;
|
||||||
|
import org.hibernate.engine.spi.PersistenceContext;
|
||||||
import org.hibernate.engine.spi.SessionImplementor;
|
import org.hibernate.engine.spi.SessionImplementor;
|
||||||
import org.hibernate.engine.spi.Status;
|
import org.hibernate.engine.spi.Status;
|
||||||
import org.hibernate.internal.CoreLogging;
|
import org.hibernate.internal.CoreLogging;
|
||||||
import org.hibernate.internal.CoreMessageLogger;
|
import org.hibernate.internal.CoreMessageLogger;
|
||||||
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
|
|
||||||
public enum EntityState {
|
public enum EntityState {
|
||||||
PERSISTENT, TRANSIENT, DETACHED, DELETED;
|
PERSISTENT, TRANSIENT, DETACHED, DELETED;
|
||||||
|
@ -65,6 +68,16 @@ public enum EntityState {
|
||||||
if ( LOG.isTraceEnabled() ) {
|
if ( LOG.isTraceEnabled() ) {
|
||||||
LOG.tracev( "Detached instance of: {0}", EventUtil.getLoggableName( entityName, entity ) );
|
LOG.tracev( "Detached instance of: {0}", EventUtil.getLoggableName( entityName, entity ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final PersistenceContext persistenceContext = source.getPersistenceContextInternal();
|
||||||
|
if ( persistenceContext.containsDeletedUnloadedEntityKeys() ) {
|
||||||
|
final EntityPersister entityPersister = source.getEntityPersister( entityName, entity );
|
||||||
|
final Object identifier = entityPersister.getIdentifier( entity, source );
|
||||||
|
final EntityKey entityKey = source.generateEntityKey( identifier, entityPersister );
|
||||||
|
if ( persistenceContext.containsDeletedUnloadedEntityKey( entityKey ) ) {
|
||||||
|
return EntityState.DELETED;
|
||||||
|
}
|
||||||
|
}
|
||||||
return DETACHED;
|
return DETACHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ package org.hibernate.event.spi;
|
||||||
import org.hibernate.HibernateException;
|
import org.hibernate.HibernateException;
|
||||||
import org.hibernate.engine.spi.ActionQueue;
|
import org.hibernate.engine.spi.ActionQueue;
|
||||||
import org.hibernate.engine.spi.EntityEntry;
|
import org.hibernate.engine.spi.EntityEntry;
|
||||||
|
import org.hibernate.engine.spi.EntityKey;
|
||||||
import org.hibernate.engine.spi.SessionImplementor;
|
import org.hibernate.engine.spi.SessionImplementor;
|
||||||
import org.hibernate.persister.entity.EntityPersister;
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
|
|
||||||
|
@ -32,6 +33,10 @@ public interface EventSource extends SessionImplementor {
|
||||||
* Force an immediate flush
|
* Force an immediate flush
|
||||||
*/
|
*/
|
||||||
void forceFlush(EntityEntry e) throws HibernateException;
|
void forceFlush(EntityEntry e) throws HibernateException;
|
||||||
|
/**
|
||||||
|
* Force an immediate flush
|
||||||
|
*/
|
||||||
|
void forceFlush(EntityKey e) throws HibernateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cascade merge an entity instance
|
* Cascade merge an entity instance
|
||||||
|
|
|
@ -1427,18 +1427,23 @@ public class SessionImpl
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void forceFlush(EntityEntry entityEntry) throws HibernateException {
|
public void forceFlush(EntityEntry entityEntry) throws HibernateException {
|
||||||
|
forceFlush( entityEntry.getEntityKey() );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void forceFlush(EntityKey key) throws HibernateException {
|
||||||
if ( log.isDebugEnabled() ) {
|
if ( log.isDebugEnabled() ) {
|
||||||
log.debugf(
|
log.debugf(
|
||||||
"Flushing to force deletion of re-saved object: %s",
|
"Flushing to force deletion of re-saved object: %s",
|
||||||
MessageHelper.infoString( entityEntry.getPersister(), entityEntry.getId(), getFactory() )
|
MessageHelper.infoString( key.getPersister(), key.getIdentifier(), getFactory() )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( persistenceContext.getCascadeLevel() > 0 ) {
|
if ( persistenceContext.getCascadeLevel() > 0 ) {
|
||||||
throw new ObjectDeletedException(
|
throw new ObjectDeletedException(
|
||||||
"deleted object would be re-saved by cascade (remove deleted object from associations)",
|
"deleted object would be re-saved by cascade (remove deleted object from associations)",
|
||||||
entityEntry.getId(),
|
key.getIdentifier(),
|
||||||
entityEntry.getPersister().getEntityName()
|
key.getPersister().getEntityName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
checkOpenOrWaitingForAutoClose();
|
checkOpenOrWaitingForAutoClose();
|
||||||
|
|
|
@ -2,15 +2,19 @@ package org.hibernate.orm.test.deleteunloaded;
|
||||||
|
|
||||||
import org.hibernate.Transaction;
|
import org.hibernate.Transaction;
|
||||||
import org.hibernate.testing.orm.junit.DomainModel;
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
|
import org.hibernate.testing.orm.junit.JiraKey;
|
||||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.hibernate.Hibernate.isInitialized;
|
import static org.hibernate.Hibernate.isInitialized;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
@DomainModel( annotatedClasses = { Parent.class, Child.class } )
|
@DomainModel( annotatedClasses = { Parent.class, Child.class, ParentSub.class } )
|
||||||
@SessionFactory
|
@SessionFactory
|
||||||
//@ServiceRegistry(
|
//@ServiceRegistry(
|
||||||
// settings = {
|
// settings = {
|
||||||
|
@ -18,6 +22,15 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
// }
|
// }
|
||||||
//)
|
//)
|
||||||
public class DeleteUnloadedProxyTest {
|
public class DeleteUnloadedProxyTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void cleanup(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
session.createMutationQuery( "delete from ParentSub" ).executeUpdate();
|
||||||
|
session.createMutationQuery( "delete from Child" ).executeUpdate();
|
||||||
|
session.createMutationQuery( "delete from Parent" ).executeUpdate();
|
||||||
|
} );
|
||||||
|
}
|
||||||
@Test
|
@Test
|
||||||
public void testAttached(SessionFactoryScope scope) {
|
public void testAttached(SessionFactoryScope scope) {
|
||||||
Parent p = new Parent();
|
Parent p = new Parent();
|
||||||
|
@ -86,4 +99,48 @@ public class DeleteUnloadedProxyTest {
|
||||||
assertNull( em.find( Child.class, c.getId() ) );
|
assertNull( em.find( Child.class, c.getId() ) );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@JiraKey( "HHH-16690" )
|
||||||
|
public void testRePersist(SessionFactoryScope scope) {
|
||||||
|
Parent p = new Parent();
|
||||||
|
ParentSub ps = new ParentSub( 1L, "abc", p );
|
||||||
|
scope.inTransaction( em -> {
|
||||||
|
em.persist( p );
|
||||||
|
em.persist( ps );
|
||||||
|
} );
|
||||||
|
scope.inTransaction( em -> {
|
||||||
|
ParentSub sub = em.getReference( ParentSub.class, 1L );
|
||||||
|
assertFalse( isInitialized( sub ) );
|
||||||
|
em.remove( sub );
|
||||||
|
em.persist( new ParentSub( 1L, "def", p ) );
|
||||||
|
} );
|
||||||
|
scope.inSession( em -> {
|
||||||
|
ParentSub sub = em.find( ParentSub.class, 1L );
|
||||||
|
assertNotNull( sub );
|
||||||
|
assertEquals( "def", sub.getData() );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@JiraKey( "HHH-16690" )
|
||||||
|
public void testReMerge(SessionFactoryScope scope) {
|
||||||
|
Parent p = new Parent();
|
||||||
|
ParentSub ps = new ParentSub( 1L, "abc", p );
|
||||||
|
scope.inTransaction( em -> {
|
||||||
|
em.persist( p );
|
||||||
|
em.persist( ps );
|
||||||
|
} );
|
||||||
|
scope.inTransaction( em -> {
|
||||||
|
ParentSub sub = em.getReference( ParentSub.class, 1L );
|
||||||
|
assertFalse( isInitialized( sub ) );
|
||||||
|
em.remove( sub );
|
||||||
|
em.merge( new ParentSub( 1L, "def", p ) );
|
||||||
|
} );
|
||||||
|
scope.inSession( em -> {
|
||||||
|
ParentSub sub = em.find( ParentSub.class, 1L );
|
||||||
|
assertNotNull( sub );
|
||||||
|
assertEquals( "def", sub.getData() );
|
||||||
|
} );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package org.hibernate.orm.test.deleteunloaded;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class ParentSub {
|
||||||
|
@Id
|
||||||
|
private long id;
|
||||||
|
private String data;
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
private Parent parent;
|
||||||
|
|
||||||
|
public ParentSub() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParentSub(long id, String data, Parent parent) {
|
||||||
|
this.id = id;
|
||||||
|
this.data = data;
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(String data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Parent getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParent(Parent parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue