Skip to content

Commit 49a2b20

Browse files
committed
HHH-15509 enable unloaded-proxy delete for entities with owned collections
1 parent 17e8b72 commit 49a2b20

File tree

6 files changed

+102
-34
lines changed

6 files changed

+102
-34
lines changed

hibernate-core/src/main/java/org/hibernate/action/internal/CollectionRemoveAction.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public final class CollectionRemoveAction extends CollectionAction {
2929
*
3030
* Use this constructor when the collection is non-null.
3131
*
32-
* @param collection The collection to to remove; must be non-null
32+
* @param collection The collection to remove; must be non-null
3333
* @param persister The collection's persister
3434
* @param id The collection key
3535
* @param emptySnapshot Indicates if the snapshot is empty
@@ -81,18 +81,35 @@ public CollectionRemoveAction(
8181
this.affectedOwner = affectedOwner;
8282
}
8383

84+
/**
85+
* Removes a persistent collection for an unloaded proxy.
86+
*
87+
* Use this constructor when the owning entity is has not been loaded.
88+
* @param persister The collection's persister
89+
* @param id The collection key
90+
* @param session The session
91+
*/
92+
public CollectionRemoveAction(
93+
final CollectionPersister persister,
94+
final Object id,
95+
final SharedSessionContractImplementor session) {
96+
super( persister, null, id, session );
97+
emptySnapshot = false;
98+
affectedOwner = null;
99+
}
100+
84101
@Override
85102
public void execute() throws HibernateException {
86103
preRemove();
87104

88105
final SharedSessionContractImplementor session = getSession();
89106

90107
if ( !emptySnapshot ) {
91-
// an existing collection that was either non-empty or uninitialized
108+
// an existing collection that was either nonempty or uninitialized
92109
// is replaced by null or a different collection
93-
// (if the collection is uninitialized, hibernate has no way of
110+
// (if the collection is uninitialized, Hibernate has no way of
94111
// knowing if the collection is actually empty without querying the db)
95-
getPersister().remove( getKey(), session);
112+
getPersister().remove( getKey(), session );
96113
}
97114

98115
final PersistentCollection<?> collection = getCollection();

hibernate-core/src/main/java/org/hibernate/action/internal/EntityDeleteAction.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public class EntityDeleteAction extends EntityAction {
3939

4040
/**
4141
* Constructs an EntityDeleteAction.
42-
* @param id The entity identifier
42+
*
43+
* @param id The entity identifier
4344
* @param state The current (extracted) entity state
4445
* @param version The current entity version
4546
* @param instance The entity instance
@@ -61,7 +62,6 @@ public EntityDeleteAction(
6162
this.state = state;
6263

6364
NaturalIdMapping naturalIdMapping = persister.getNaturalIdMapping();
64-
6565
if ( naturalIdMapping != null ) {
6666
naturalIdValues = session.getPersistenceContextInternal().getNaturalIdResolutions()
6767
.removeLocalResolution(
@@ -72,6 +72,23 @@ public EntityDeleteAction(
7272
}
7373
}
7474

75+
/**
76+
* Constructs an EntityDeleteAction for an unloaded proxy.
77+
*
78+
* @param id The entity identifier
79+
* @param persister The entity persister
80+
* @param session The session
81+
*/
82+
public EntityDeleteAction(
83+
final Object id,
84+
final EntityPersister persister,
85+
final SessionImplementor session) {
86+
super( session, id, null, persister );
87+
this.version = null;
88+
this.isCascadeDeleteEnabled = false;
89+
this.state = null;
90+
}
91+
7592
public Object getVersion() {
7693
return version;
7794
}

hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.hibernate.HibernateException;
1212
import org.hibernate.LockMode;
1313
import org.hibernate.TransientObjectException;
14+
import org.hibernate.action.internal.CollectionRemoveAction;
1415
import org.hibernate.action.internal.EntityDeleteAction;
1516
import org.hibernate.action.internal.OrphanRemovalAction;
1617
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
@@ -21,10 +22,12 @@
2122
import org.hibernate.engine.internal.ForeignKeys;
2223
import org.hibernate.engine.internal.Nullability;
2324
import org.hibernate.engine.internal.Nullability.NullabilityCheckType;
25+
import org.hibernate.engine.spi.ActionQueue;
2426
import org.hibernate.engine.spi.CascadingActions;
2527
import org.hibernate.engine.spi.EntityEntry;
2628
import org.hibernate.engine.spi.EntityKey;
2729
import org.hibernate.engine.spi.PersistenceContext;
30+
import org.hibernate.engine.spi.SharedSessionContractImplementor;
2831
import org.hibernate.engine.spi.Status;
2932
import org.hibernate.event.service.spi.JpaBootstrapSensitive;
3033
import org.hibernate.event.spi.DeleteContext;
@@ -37,13 +40,15 @@
3740
import org.hibernate.jpa.event.spi.CallbackRegistry;
3841
import org.hibernate.jpa.event.spi.CallbackRegistryConsumer;
3942
import org.hibernate.jpa.event.spi.CallbackType;
43+
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
4044
import org.hibernate.persister.collection.CollectionPersister;
4145
import org.hibernate.persister.entity.EntityPersister;
4246
import org.hibernate.pretty.MessageHelper;
4347
import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl;
4448
import org.hibernate.proxy.HibernateProxy;
4549
import org.hibernate.proxy.LazyInitializer;
4650
import org.hibernate.type.CollectionType;
51+
import org.hibernate.type.CompositeType;
4752
import org.hibernate.type.Type;
4853
import org.hibernate.type.TypeHelper;
4954

@@ -110,17 +115,15 @@ && canBeDeletedWithoutLoading( source, persister ) ) {
110115
persistenceContext.reassociateProxy( object, id );
111116
if ( !persistenceContext.containsDeletedUnloadedEntityKey( key ) ) {
112117
persistenceContext.registerDeletedUnloadedEntityKey( key );
113-
source.getActionQueue().addAction(
114-
new EntityDeleteAction(
115-
id,
116-
null,
117-
null,
118-
null,
119-
persister,
120-
false,
121-
source
122-
)
123-
);
118+
119+
if ( persister.hasOwnedCollections() ) {
120+
// we're deleting an unloaded proxy with collections
121+
for ( Type type : persister.getPropertyTypes() ) { //TODO: when we enable this for subclasses use getSubclassPropertyTypeClosure()
122+
deleteOwnedCollections( type, id, source, source.getActionQueue() );
123+
}
124+
}
125+
126+
source.getActionQueue().addAction( new EntityDeleteAction( id, persister, source ) );
124127
}
125128
return true;
126129
}
@@ -129,6 +132,23 @@ && canBeDeletedWithoutLoading( source, persister ) ) {
129132
return false;
130133
}
131134

135+
private void deleteOwnedCollections(Type type, Object key, SharedSessionContractImplementor session, ActionQueue actionQueue) {
136+
MappingMetamodelImplementor mappingMetamodel = session.getFactory().getMappingMetamodel();
137+
if ( type.isCollectionType() ) {
138+
String role = ( (CollectionType) type ).getRole();
139+
CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role);
140+
if ( !persister.isInverse() ) {
141+
actionQueue.addAction( new CollectionRemoveAction( persister, key, session ) );
142+
}
143+
}
144+
else if ( type.isComponentType() ) {
145+
Type[] subtypes = ( (CompositeType) type ).getSubtypes();
146+
for ( Type subtype : subtypes ) {
147+
deleteOwnedCollections( subtype, key, session, actionQueue );
148+
}
149+
}
150+
}
151+
132152
private void delete(DeleteEvent event, DeleteContext transientEntities) {
133153
final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal();
134154
final Object entity = persistenceContext.unproxyAndReassociate( event.getObject() );
@@ -252,9 +272,8 @@ private void delete(
252272
private boolean canBeDeletedWithoutLoading(EventSource source, EntityPersister persister) {
253273
return source.getInterceptor() == EmptyInterceptor.INSTANCE
254274
&& !persister.implementsLifecycle()
255-
&& !persister.hasSubclasses()
275+
&& !persister.hasSubclasses() //TODO: should be unnecessary, using EntityPersister.getSubclassPropertyTypeClosure(), etc
256276
&& !persister.hasCascadeDelete()
257-
&& !persister.hasOwnedCollections()
258277
&& !persister.hasNaturalIdentifier()
259278
&& !hasRegisteredRemoveCallbacks( persister )
260279
&& !hasCustomEventListeners( source );

hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3820,7 +3820,7 @@ public void update(
38203820
if ( entry == null && !isMutable() ) {
38213821
throw new IllegalStateException( "Updating immutable entity that is not in session yet" );
38223822
}
3823-
if ( ( entityMetamodel.isDynamicUpdate() && dirtyFields != null ) ) {
3823+
if ( entityMetamodel.isDynamicUpdate() && dirtyFields != null ) {
38243824
// We need to generate the UPDATE SQL when dynamic-update="true"
38253825
propsToUpdate = getPropertiesToUpdate( dirtyFields, hasDirtyCollection );
38263826
// don't need to check laziness (dirty checking algorithm handles that)
@@ -4014,41 +4014,42 @@ protected void preInsertInMemoryValueGeneration(Object[] fields, Object object,
40144014
@Override
40154015
public void delete(Object id, Object version, Object object, SharedSessionContractImplementor session)
40164016
throws HibernateException {
4017-
final int span = getTableSpan();
40184017
boolean isImpliedOptimisticLocking = !entityMetamodel.isVersioned() && isAllOrDirtyOptLocking()
40194018
&& object != null; // null object signals that we're deleting an unloaded proxy
4019+
40204020
Object[] loadedState = null;
40214021
if ( isImpliedOptimisticLocking ) {
40224022
// need to treat this as if it where optimistic-lock="all" (dirty does *not* make sense);
40234023
// first we need to locate the "loaded" state
40244024
//
40254025
// Note, it potentially could be a proxy, so doAfterTransactionCompletion the location the safe way...
4026-
final EntityKey key = session.generateEntityKey( id, this );
40274026
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
4028-
Object entity = persistenceContext.getEntity( key );
4027+
Object entity = persistenceContext.getEntity( session.generateEntityKey( id, this ) );
40294028
if ( entity != null ) {
40304029
EntityEntry entry = persistenceContext.getEntry( entity );
40314030
loadedState = entry.getLoadedState();
40324031
}
40334032
}
40344033

4035-
final String[] deleteStrings;
4034+
final String[] deleteStrings = getSQLDeleteStrings( object, isImpliedOptimisticLocking, loadedState );
4035+
for ( int j = getTableSpan() - 1; j >= 0; j-- ) {
4036+
delete( id, version, j, object, deleteStrings[j], session, loadedState );
4037+
}
4038+
}
4039+
4040+
private String[] getSQLDeleteStrings(Object object, boolean isImpliedOptimisticLocking, Object[] loadedState) {
40364041
if ( isImpliedOptimisticLocking && loadedState != null ) {
40374042
// we need to utilize dynamic delete statements
4038-
deleteStrings = generateSQLDeleteStrings( loadedState );
4043+
return generateSQLDeleteStrings(loadedState);
40394044
}
4040-
else if (object!=null) {
4045+
else if ( object != null ) {
40414046
// otherwise, utilize the static delete statements
4042-
deleteStrings = getSQLDeleteStrings();
4047+
return getSQLDeleteStrings();
40434048
}
40444049
else {
4045-
deleteStrings = getSQLDeleteNoVersionCheckStrings();
4050+
// deleting an unloaded proxy
4051+
return getSQLDeleteNoVersionCheckStrings();
40464052
}
4047-
4048-
for ( int j = span - 1; j >= 0; j-- ) {
4049-
delete( id, version, j, object, deleteStrings[j], session, loadedState );
4050-
}
4051-
40524053
}
40534054

40544055
protected boolean isAllOrDirtyOptLocking() {
@@ -4069,7 +4070,7 @@ protected String[] generateSQLDeleteStrings(Object[] loadedState) {
40694070
Type[] types = getPropertyTypes();
40704071
for ( int i = 0; i < entityMetamodel.getPropertySpan(); i++ ) {
40714072
if ( isPropertyOfTable( i, j ) && versionability[i] ) {
4072-
// this property belongs to the table and it is not specifically
4073+
// This property belongs to the table, and it's not explicitly
40734074
// excluded from optimistic locking by optimistic-lock="false"
40744075
String[] propertyColumnNames = getPropertyColumnNames( i );
40754076
boolean[] propertyNullness = types[i].toColumnNullness( loadedState[i], getFactory() );

hibernate-core/src/test/java/org/hibernate/orm/test/deleteunloaded/DeleteUnloadedProxyTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public void testAttached(SessionFactoryScope scope) {
2626
Transaction tx = em.beginTransaction();
2727
c.setParent(p);
2828
p.getChildren().add(c);
29+
p.getWords().add("hello");
30+
p.getWords().add("world");
2931
em.persist(p);
3032
tx.commit();
3133
} );
@@ -54,6 +56,8 @@ public void testDetached(SessionFactoryScope scope) {
5456
Transaction tx = em.beginTransaction();
5557
c.setParent(p);
5658
p.getChildren().add(c);
59+
p.getWords().add("hello");
60+
p.getWords().add("world");
5761
em.persist(p);
5862
tx.commit();
5963
} );

hibernate-core/src/test/java/org/hibernate/orm/test/deleteunloaded/Parent.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package org.hibernate.orm.test.deleteunloaded;
22

33
import jakarta.persistence.CascadeType;
4+
import jakarta.persistence.ElementCollection;
45
import jakarta.persistence.Entity;
56
import jakarta.persistence.GeneratedValue;
67
import jakarta.persistence.Id;
78
import jakarta.persistence.OneToMany;
89
import jakarta.persistence.Version;
910

11+
import java.util.ArrayList;
1012
import java.util.HashSet;
13+
import java.util.List;
1114
import java.util.Set;
1215

1316
@Entity
@@ -21,10 +24,17 @@ public class Parent {
2124
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
2225
private Set<Child> children = new HashSet<>();
2326

27+
@ElementCollection
28+
private List<String> words = new ArrayList<>();
29+
2430
public Set<Child> getChildren() {
2531
return children;
2632
}
2733

34+
public List<String> getWords() {
35+
return words;
36+
}
37+
2838
public long getId() {
2939
return id;
3040
}

0 commit comments

Comments
 (0)