Unlink a bidirectional 1:n mapping without orphan removal by JPA

52 Views Asked by At

I try to unlink a bidirectional relationship with a 1:n mapping, but would like to avoid JPA orphan removal in certain cases.

I'm using Spring Boot 2.7.15, Eclipselink 2.7.13 and Java 17.

Here's the model:

@Entity
public class Parent {

    @Id
    private Integer id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<Child> children = new ArrayList<>();

    public int unlinkOddChildren() {
        var oddChildren = getOddChildren();
        oddChildren.forEach(Child::unlink); // unlink all odd children
        return oddChildren.size();
    }

    public void notifyOnUnlink(Child child) {
        this.getChildren().remove(child); // remove unlinked children from parent list
    }

    public Collection<Child> getOddChildren() {
        final Collection<Child> oddChildren = new ArrayList<>(this.getChildren());
        oddChildren.removeAll(this.getChildren().stream()
                .filter(child -> child.getId() % 2 == 0)
                .toList());
        return oddChildren;
    }

    // getters and setters
    // ...
}
@Entity
public class Child {

    @Id
    private Integer id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "parent_id")
    private Parent parent;

    public void unlink() {
        final var formerParent = getParent(); // Keep a backup reference to parent
        setParent(null);
        formerParent.notifyOnUnlink(this);
    }

    // getters and setters
    // ...
}

The goal is that the state in the Java layer is always consistent, without having to first flush/persist and then reload the objects from the database. (The flush/reload approach would also make unit tests for this case very complex, i.e. they would require a DB connection and Spring context, although one only wants to test if unlinkOddChildren() works.)

Given following example:

parent.getChildren(); // returns the parent's children 1-5 (a list of 5 children)
parent.unlinkOddChildren(); // returns the number of unlinked odd children
parent.getChildren(); // returns children 2 and 4

This works because unlinkOddChildren() is implemented like this: once a child is unlinked, it notifies the parent and then is removed from the parent's list of children.

The problem I am trying to solve is this:

Once the changes are saved to the DB, the children 1, 3 and 5 would be deleted due to orphanRemoval = true setting in the @OneToMany mapping. But I just need to unlink them, so they continue to exist on the DB and are available for re-assignment to a new/other parent at a later point in time:

var orphans = findAllChildrenWithoutParent(); // returns all children on DB not related to a parent
var parent = new Parent();
parent.adopt(orphans);

Solutions that do not work in this scenario:

  1. setting orphanRemoval = false: in other scenarios, it is expected to auto-delete the orphaned children, e.g. when a parent is deleted.
  2. child.setParent(null) without notifying the parent and removing the child from the parent's children list: this would violate the previously described expectation, that the state in the Java layer is always consistent. When a child is unlinked from its parent, the parent should be updated with this information, i.e. its list of children needs to be updated within the process of unlinking. In other words: it may never be that a parent holds a reference to a child that does not hold a reference to its parent.

The question is:

How can I make JPA ignore the orphanRemoval = true setting in this particular case, so the orphans are not deleted?

Note: I uploaded a maven project with the code to GitHub, in case someone is interested in playing around with it: https://github.com/fkriegl/bidirectional-unlink-without-orphan-removal


Before composing this question on stackoverflow, I tried to
- find a solution by reading the JPA/Eclipselink documentation for orphanRemoval.
- set orphanRemoval = false, but this did break existing code/tests that rely on orphan removal. Manually deleting the orphans is not really feasible, it is too error prone.

0

There are 0 best solutions below