diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java index 778dce73413e..4e718070bb5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -94,6 +95,9 @@ public boolean isSnapshotEmpty(Serializable snapshot) { public Collection getOrphans(Serializable snapshot, String entityName) throws HibernateException { final Object[] sn = (Object[]) snapshot; final Object[] arr = (Object[]) array; + if ( arr.length == 0 ) { + return Arrays.asList( sn ); + } final ArrayList result = new ArrayList(); Collections.addAll( result, sn ); for ( int i=0; i void cascadeCollectionElements( } } + // a newly instantiated collection can't have orphans + final PersistentCollection persistentCollection; + if ( child instanceof PersistentCollection ) { + persistentCollection = (PersistentCollection) child; + } + else { + persistentCollection = eventSource.getPersistenceContext() + .getCollectionHolder( child ); + } + final boolean deleteOrphans = style.hasOrphanDelete() && action.deleteOrphans() && elemType instanceof EntityType - && child instanceof PersistentCollection + && persistentCollection != null // a newly instantiated collection can't have orphans - && ! ( (PersistentCollection) child ).isNewlyInstantiated(); + && !persistentCollection.isNewlyInstantiated(); if ( deleteOrphans ) { final boolean traceEnabled = LOG.isTraceEnabled(); @@ -637,7 +647,7 @@ private static void cascadeCollectionElements( // 1. newly instantiated collections // 2. arrays (we can't track orphans for detached arrays) final String entityName = collectionType.getAssociatedEntityName( eventSource.getFactory() ); - deleteOrphans( eventSource, entityName, (PersistentCollection) child ); + deleteOrphans( eventSource, entityName, persistentCollection ); if ( traceEnabled ) { LOG.tracev( "Done deleting orphans for collection: {0}", collectionType.getRole() ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java index b127e764b2b2..ef2c37f24262 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java @@ -25,6 +25,7 @@ import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.collection.spi.AbstractPersistentCollection; +import org.hibernate.collection.spi.PersistentArrayHolder; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.CollectionKey; @@ -641,11 +642,36 @@ public Object replace( if ( target == null ) { return null; } - if ( target instanceof PersistentCollection ) { - final Collection collection = (Collection) target; - collection.clear(); - return collection; + if ( target instanceof Collection ) { + ( (Collection) target ).clear(); + return target; } + else if ( target instanceof Map ) { + ( (Map) target ).clear(); + return target; + } + else { + final PersistenceContext persistenceContext = session.getPersistenceContext(); + final PersistentCollection collectionHolder = persistenceContext + .getCollectionHolder( target ); + if ( collectionHolder != null ) { + if ( collectionHolder instanceof PersistentArrayHolder ) { + PersistentArrayHolder persistentArrayHolder = (PersistentArrayHolder) collectionHolder; + persistenceContext.removeCollectionHolder( target ); + persistentArrayHolder.beginRead(); + persistentArrayHolder.injectLoadedState( + persistenceContext.getCollectionEntry( collectionHolder ) + .getLoadedPersister() + .getAttributeMapping(), null + ); + persistentArrayHolder.endRead(); + persistentArrayHolder.dirty(); + persistenceContext.addCollectionHolder( collectionHolder ); + return persistentArrayHolder.getArray(); + } + } + } + return null; } if ( !Hibernate.isInitialized( original ) ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java new file mode 100644 index 000000000000..1675d935f536 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +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.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeCascadeWithMapCollectionTest.Parent.class, + MergeCascadeWithMapCollectionTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18842") +public class MergeCascadeWithMapCollectionTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE) + private Map children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + 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 Map getChildren() { + return children; + } + + public void setChildren(Map children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new HashMap<>(); + } + children.put( child.getName(), child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + 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; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java new file mode 100644 index 000000000000..9f2cc05f2a15 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +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.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalArrayBidirectionalTest.Parent.class, + MergeOrphanRemovalArrayBidirectionalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18842") +public class MergeOrphanRemovalArrayBidirectionalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + Child[] children = new Child[1]; + children[0] = child; + parent2.setChildren( children ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + Child[] children = new Child[1]; + children[0] = child; + parent.setChildren( children ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().length ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().length ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true, mappedBy = "parent") + private Child[] children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + 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 Child[] getChildren() { + return children; + } + + public void setChildren(Child[] children) { + this.children = children; + for ( int i = 0; i < children.length; i++ ) { + children[i].setParent( this ); + } + } + + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = 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; + } + } + +}