简体   繁体   中英

Keeping a bidirectional parent-child collection up to date

I am trying to keep the collections of the entities up to date with the internal database structure but failing to do so with a bidirectional, cascade-delete relation between Parent and Child.

  • Deleting a parent should cascade-delete all children
  • Addition and deletion of a child should be reflected in the parent's getChildren() set

The code below works if there is only one child, any more than that and I get ConcurrentModificationException , which is logical since Hibernate iterates over the collection when cascading.

If I remove the @PreRemove the removeChild test below fails.

Any suggestions on how to solve this without adding a specific deleteChild method that performs the clean up? I am trying to avoid having any clean-up methods outside of the entities.

@Entity
public class Parent {
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.REMOVE)
    private Set<Child> children = new HashSet<>();

    public Set<Child> getChildren() {
        return Collections.unmodifiableSet(children);
    }

    void internalAddChild(final Child child) {
        children.add(child);
    }

    void internalRemoveChild(final Child child) {
        children.remove(child);
    }
}

@Entity
public class Child {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id", nullable = false)
    private Parent parent;

    public Child(final Parent parent) {
        setParent(parent);
    }

    public final void setParent(final Parent parent) {
        if (this.parent != null) {
            this.parent.internalRemoveChild(this);
        }

        this.parent = parent;

        if (parent != null) {
            parent.internalAddChild(this);
        }
    }

    @PreRemove
    private void preRemove() {
        // Causes ConcurrentModificationException in test removeParent below
        if (parent != null) {
            parent.internalRemoveChild(this);
        }
    }
}   

Tests:

@Test
public void removeParent() {
    EntityManager em = getEntityManager()
    Parent parent = new Parent();
    em.persist(parent);
    em.persist(new Child(parent));
    em.persist(new Child(parent));

    assertTrue(parent.getChildren().size() == 2);

    // Causes ConcurrentModificationException if more than 1 child
    em.remove(parent);

    // Both children should be deleted
}

@Test
public void removeChild() {
    EntityManager em = getEntityManager()
    Parent parent = new Parent();
    em.persist(parent);

    Child child = new Child(parent);
    em.persist(child);

    em.remove(child);

    // Fails without @PreRemove in Child, child is still present in set
    assertFalse(parent.getChildren().contains(child));
}

Exception stack trace:

java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
    at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
    at org.hibernate.collection.internal.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:789)
    at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:379)
    at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:319)
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:296)
    at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161)
    at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:118)
    at org.hibernate.event.internal.DefaultDeleteEventListener.cascadeBeforeDelete(DefaultDeleteEventListener.java:353)
    at org.hibernate.event.internal.DefaultDeleteEventListener.deleteEntity(DefaultDeleteEventListener.java:275)
    at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:160)
    at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:73)
    at org.hibernate.internal.SessionImpl.fireDelete(SessionImpl.java:920)
    at org.hibernate.internal.SessionImpl.delete(SessionImpl.java:896)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.remove(AbstractEntityManagerImpl.java:1214)
    ...

Try putting orphanRemoval = true on @OneToMany mapping, and remove CascadeType.REMOVE since it is now redundant. This instructs the persistence provider to remove child entities when parent is deleted, or their relation is set to null.

One side note (may affect this problem but doesn't have to, it's just good practice) is to avoid wiring up the relations in the constructor (like you do now in Child , and instead move the logic to some kind of addChild and removeChild methods ( internalRemoveChild and internalAddChild in your case). It would look like this

void internalAddChild(final Child child) {
    if (child != null) {
        child.setParent(this);
        children.add(child);
    }
}

void internalRemoveChild(final Child child) {
    if (child != null) {
        children.remove(child);
        child.setParent(null);
    }
}

// test code

Parent parent = new Parent();
Child c1 = new Child();
Child c2 = new Child();
parent.internalAddChild(c1);
parent.internalAddChild(c2);
em.persist(parent);
em.persist(c1);
em.persist(c2);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM