HHH-14387 Don't create PersistentCollection for bytecode enhanced lazy loaded attributes and make sure collection deletes still work

This commit is contained in:
Christian Beikov 2022-08-18 11:14:21 +02:00
parent b18c967cf6
commit 41ac1f8e88
7 changed files with 295 additions and 26 deletions

View File

@ -13,9 +13,11 @@ import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadingAction;
import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.PersistenceContext;
@ -79,7 +81,11 @@ public final class Cascade {
final EntityPersister persister,
final Object parent,
final T anything) throws HibernateException {
if ( action == CascadingActions.DELETE && cascadePoint == CascadePoint.AFTER_INSERT_BEFORE_DELETE ) {
// Before deleting an entity, ensure CollectionEntry objects for uninitialized lazy collections exist,
// otherwise these collections are not properly deleted and this leads to FK violations
registerUninitializedLazyCollectionEntries( eventSource, persister, parent );
}
if ( persister.hasCascades() || action.requiresNoCascadeChecking() ) { // performance opt
final boolean traceEnabled = LOG.isTraceEnabled();
if ( traceEnabled ) {
@ -196,6 +202,30 @@ public final class Cascade {
}
}
private static void registerUninitializedLazyCollectionEntries(EventSource eventSource, EntityPersister persister, Object parent) {
if ( !persister.hasCollections() || !persister.hasUninitializedLazyProperties( parent ) ) {
return;
}
final Type[] types = persister.getPropertyTypes();
final String[] propertyNames = persister.getPropertyNames();
final BytecodeEnhancementMetadata enhancementMetadata = persister.getBytecodeEnhancementMetadata();
for ( int i = 0; i < types.length; i++) {
if ( types[i].isCollectionType() && !enhancementMetadata.isAttributeLoaded( parent, propertyNames[i] ) ) {
final CollectionType collectionType = (CollectionType) types[i];
final CollectionPersister collectionDescriptor = persister.getFactory()
.getRuntimeMetamodels()
.getMappingMetamodel()
.getCollectionDescriptor( collectionType.getRole() );
if ( collectionDescriptor.needsRemove() || collectionDescriptor.hasCache() ) {
final Object keyOfOwner = collectionType.getKeyOfOwner( parent, eventSource.getSession() );
// This will make sure that a CollectionEntry exists
collectionType.getCollection( keyOfOwner, eventSource.getSession(), parent, false );
}
}
}
}
/**
* Cascade an action to the child or children
*/

View File

@ -1523,6 +1523,11 @@ public abstract class AbstractCollectionPersister
return true;
}
@Override
public boolean needsRemove() {
return !isInverse() && isRowDeleteEnabled();
}
private BasicBatchKey deleteBatchKey;
@Override

View File

@ -98,6 +98,14 @@ public interface CollectionPersister extends Restrictable {
*/
boolean hasCache();
/**
* Whether {@link #remove(Object, SharedSessionContractImplementor)} might actually do something,
* or if it is definitely a no-op.
*/
default boolean needsRemove() {
return true;
}
/**
* Get the cache
*/

View File

@ -38,12 +38,7 @@ public class DelayedCollectionFetch extends CollectionFetch {
AssemblerCreationState creationState) {
// lazy attribute
if ( collectionKeyResult == null ) {
return new UnfetchedCollectionAssembler(
getNavigablePath(),
getFetchedMapping(),
parentAccess,
creationState
);
return new UnfetchedCollectionAssembler( getFetchedMapping() );
}
else {
return new DelayedCollectionAssembler(

View File

@ -24,6 +24,7 @@ public class DelayedCollectionInitializer extends AbstractCollectionInitializer
FetchParentAccess parentAccess,
DomainResultAssembler<?> collectionKeyResultAssembler) {
super( fetchedPath, fetchedMapping, parentAccess, collectionKeyResultAssembler );
assert collectionKeyResultAssembler != null;
}
@Override

View File

@ -8,34 +8,17 @@ package org.hibernate.sql.results.graph.collection.internal;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.results.graph.AssemblerCreationState;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
import org.hibernate.type.descriptor.java.JavaType;
public class UnfetchedCollectionAssembler implements DomainResultAssembler {
private final PluralAttributeMapping fetchedMapping;
public UnfetchedCollectionAssembler(
NavigablePath fetchPath,
PluralAttributeMapping fetchedMapping,
FetchParentAccess parentAccess,
AssemblerCreationState creationState) {
public UnfetchedCollectionAssembler(PluralAttributeMapping fetchedMapping) {
this.fetchedMapping = fetchedMapping;
creationState.resolveInitializer(
fetchPath,
fetchedMapping,
() -> new DelayedCollectionInitializer(
fetchPath,
fetchedMapping,
parentAccess,
null
)
);
}
@Override

View File

@ -0,0 +1,247 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.bytecode.enhancement.detached;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.Hibernate;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.EnhancementOptions;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.transaction.TransactionUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
/**
* @author Christian Beikov
*/
@TestForIssue(jiraKey = "HHH-14387")
@RunWith( BytecodeEnhancerRunner.class )
@EnhancementOptions(
lazyLoading = true,
inlineDirtyChecking = true,
biDirectionalAssociationManagement = true
)
public class RemoveUninitializedLazyCollectionTest extends BaseCoreFunctionalTestCase {
@Override
public Class<?>[] getAnnotatedClasses() {
return new Class<?>[]{
Parent.class,
Child1.class,
Child2.class
};
}
@After
public void tearDown() {
TransactionUtil.doInJPA(
this::sessionFactory,
session -> {
session.createQuery( "delete from Child1" ).executeUpdate();
session.createQuery( "delete from Child2" ).executeUpdate();
session.createQuery( "delete from Parent" ).executeUpdate();
}
);
}
@Before
public void setup() {
Parent parent = new Parent( 1L, "test" );
TransactionUtil.doInJPA(
this::sessionFactory,
entityManager -> {
entityManager.persist( parent );
entityManager.persist( new Child2( 1L, "child2", parent ) );
}
);
}
@Test
public void testDeleteParentWithBidirOrphanDeleteCollectionBasedOnPropertyRef() {
EntityManager em = sessionFactory().createEntityManager();
try {
// Lazily initialize the child1 collection
List<Child1> child1 = em.find( Parent.class, 1L ).getChild1();
Hibernate.initialize( child1 );
org.hibernate.testing.orm.transaction.TransactionUtil.inTransaction(
em,
entityManager -> {
Parent parent = new Parent();
parent.setId( 1L );
parent.setName( "new name" );
entityManager.merge( parent );
}
);
}
finally {
em.close();
}
}
@Entity(name = "Parent")
public static class Parent {
private Long id;
private String name;
private List<Child1> child1 = new ArrayList<>();
private List<Child2> child2 = new ArrayList<>();
public Parent() {
}
public Parent(Long id, String name) {
this.id = id;
this.name = name;
}
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@OneToMany(orphanRemoval = true, cascade = CascadeType.ALL, mappedBy = "parent")
public List<Child1> getChild1() {
return child1;
}
public void setChild1(List<Child1> child1) {
this.child1 = child1;
}
@OneToMany(orphanRemoval = true, cascade = CascadeType.ALL, mappedBy = "parent")
public List<Child2> getChild2() {
return child2;
}
public void setChild2(List<Child2> child2) {
this.child2 = child2;
}
}
@Entity(name = "Child1")
public static class Child1 {
@Id
private Long id;
private String name;
@ManyToOne
@JoinColumn
private Parent parent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
@Override
public String toString() {
return "Child1 [id=" + id + ", name=" + name + "]";
}
}
@Entity(name = "Child2")
public static class Child2 {
@Id
private Long id;
private String name;
@ManyToOne
@JoinColumn
private Parent parent;
public Child2() {
}
public Child2(Long id, String name, Parent parent) {
this.id = id;
this.name = name;
this.parent = parent;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
}