HHH-14387 Don't create PersistentCollection for bytecode enhanced lazy loaded attributes and make sure collection deletes still work
This commit is contained in:
parent
b18c967cf6
commit
41ac1f8e88
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -1523,6 +1523,11 @@ public abstract class AbstractCollectionPersister
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsRemove() {
|
||||
return !isInverse() && isRowDeleteEnabled();
|
||||
}
|
||||
|
||||
private BasicBatchKey deleteBatchKey;
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -24,6 +24,7 @@ public class DelayedCollectionInitializer extends AbstractCollectionInitializer
|
|||
FetchParentAccess parentAccess,
|
||||
DomainResultAssembler<?> collectionKeyResultAssembler) {
|
||||
super( fetchedPath, fetchedMapping, parentAccess, collectionKeyResultAssembler );
|
||||
assert collectionKeyResultAssembler != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue