JPA Orphan Removal


Published: 2020-01-07
Updated: 2020-01-08
Web: https://fritzthecat-blog.blogspot.com/2020/01/jpa-orphan-removal.html


The annotation-attribute orphanRemoval is an object-relational-mapping option that is available in JPA since version 2.0. Many people, including myself, have problems to keep this apart from the cascade-all option.

Please refer to my previous articles about JPA to find Java sources that may not be listed completely here.

Example Entity Classes

Following UML class diagram shows example relations so that we can understand orphans.

What is an Orphan?

An orphan is an entity (or database record) that once was related to another entity, but is not any more. That means, the owner still exists, only the orphan was removed from its relation collection, and now the orphan exists only in database:

....
transaction.begin();
Team team = ....;
Responsibility responsibility = ...;
team.getResponsibilities().remove(responsibility);
responsibility.setTeam(null);
transaction.commit();
....

This code creates a Responsibility orphan. The Team entity holds a collection of Responsibility. When you remove a responsibility from the team's collection and commit the current transaction, the removed responsibility would still be present in database, because the default for orphanRemoval is false.

Why Not Cascade-All?

Cascading all actions just means that when the owner gets saved/updated/removed, also its children would be saved/updated/removed. The attribute cascade = CascadeType.ALL alone would not remove the orphan, unless the owning team itself gets deleted:

....
transaction.begin();
Team team = ....;
entityManager.remove(team);
transaction.commit();
....

For this use-case, with cascade-all, you wouldn't need the orphanRemoval attribute. In other words, when you never remove a responsibility from a team and instead always remove the whole team, then cascade = CascadeType.ALL would be sufficient to also remove responsibilities.

What Is Orphan-Removal?

Orphans could be quite useful, for instance to relate them once again later. But in cases of bidirectional association between owner and child-entity it is mostly wanted that

the orphan gets removed also from database when it was removed from the owner's collection.

For that use-case you explicitly need to set orphanRemoval = true. Mind that the owner-entity still exists afterwards!

Demo Source

Following persistence classes implement what the UML class diagram above shows.

@Entity
public class Team extends BaseEntity
{
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Responsibility> responsibilities = new HashSet<>();

@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private List<Resource> resources = new ArrayList<>();

....
}

The Team class has a bidirectional relation to Responsibility (backlink present), and a unidirectional relation to Resource (no backlink).

@Entity
public class Responsibility extends BaseEntity
{
private String name;

@ManyToOne(optional = false)
private Team team;

@ManyToOne(optional = false)
private Person person;

....
}

A Responsibility is the m:n relation-table between Team and Person, whereby persons should not be deleted when responsibilities get deleted. Responsibility has a mandatory relation to Person, but there is no backlink in person:

@Entity
public class Person extends BaseEntity
{
private String name;

....
}

The Person entity is here to show that cascaded child-entities like Responsibility can refer to other entities that are not cascaded children.

@Entity
public class Resource extends BaseEntity
{
private String name;

....
}

Resources are an example for intended orphans. The application wants to create resources together with teams, but won't clean up resources when they get removed from the team, or the referencing team gets deleted.

The m:n relation-table between Team and Resource would be generated by the JPA-layer, there is no explicit class-representation for this relation-table.

Orphan-Removal Unit Test

The unit test that asserts the orphan-removal will use following test data:

This is about team - responsibility, not about team - resource. The test will create this graph, then remove the "Administrator" responsibility from the team. Then it will assert that it was deleted also in database, and that the related person is still present.

For seeing how to use following abstract test with both Hibernate and EclipseLink please look at my recent article about this, there you also may find methods missing here.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public abstract class JpaTest
{
private EntityManager em;

/** @return the name of the persistence-unit to use for all tests. */
protected abstract String getPersistenceUnitName();

@Before
public void setUp() {
em = Persistence.createEntityManagerFactory(getPersistenceUnitName()).createEntityManager();
}

@After
public void tearDown() {
for (Team team : findTeams())
transactional(em::remove, team);
for (Responsibility responsibility : findResponsibilities())
transactional(em::remove, responsibility);
for (Person person : findPersons())
transactional(em::remove, person);
for (Resource resource : findResources())
transactional(em::remove, resource);
}

@Test
public void shouldDeleteOrphanedResponsibilities() {
// create persons
final Person peter = newPerson("Peter");
transactional(em::persist, peter);
final Person mary = newPerson("Mary");
transactional(em::persist, mary);

// create a team
final Team team = new Team();
final Responsibility developer = newResponsibility("Developer", peter);
team.getResponsibilities().add(developer);
final Responsibility administrator = newResponsibility("Administrator", mary);
team.getResponsibilities().add(administrator);

transactional(em::persist, team); // cascades to also saving responsibilities
assertEquals(2, findResponsibilities().size());

// remove one responsibility from team and commit transaction
transactional(
(r) -> team.getResponsibilities().remove(r),
developer);

// assert that the orphan was deleted in database
final List<Responsibility> responsibilities = findResponsibilities();
assertEquals(1, responsibilities.size());
assertEquals(administrator, responsibilities.iterator().next());

// assert that the person related to the orphan is still present
assertEquals(2, findPersons().size());
}

....
}

The setUp() creates an entity-manager for persistence operations. The tearDown() deletes all records from all involved database tables, so that any other test in the same class can rely on an empty database.

On line 28 the tests starts to build the test data. Persons have to be stored separately as prerequisite for responsibilites. On line 34 the team gets built, and saved on line 40. Afterwards two responsibilites must exist, the "Developer" and the "Administrator".

Now the "Developer" gets removed from the team's collection on line 44 - 46, and, without explicitly storing the team, the running transaction is committed. Transparent persistence makes sure that the all changes get written to database.

The test then makes sure that only one responsibility is left in database, and that it is the "Administrator". Further it ensures that the "Developer" person is still present in database, just the relation should be gone.


When you run this test you will see that it succeeds as long as the orphanRemoval = true annotation attribute is on the Team.responsibilities relation. This would work even without cascade = CascadeType.ALL. Try to remove orphanRemoval, and watch how the test then fails.

Non-Orphan-Removal Unit Test

The opposite use-case is Team.ressources. Here we want "orphans". There is no backlink in Ressource, thus it is not a hierarchy but a m:n relation between Team and Resource. Put this method into the JpaTest unit-test class shown above.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    ....

@Test
public void shouldNotDeleteOrphanedResources() {
// create resources
final Resource printer = newResource("Printer");
final Resource scanner = newResource("Scanner");
// create a team
final Team team = new Team();
team.getResources().add(printer);
team.getResources().add(scanner);

transactional(em::persist, team); // cascades to also saving resources
assertEquals(2, findResources().size());

// remove one resource from team and commit transaction
transactional(
(p) -> team.getResources().remove(p),
printer);

// assert that no persistent relation to printer exists any more
final Team persistentTeam = findTeams().get(0);
assertEquals(1, persistentTeam.getResources().size());
assertEquals(scanner, persistentTeam.getResources().iterator().next());

// assert that the printer orphan was NOT deleted in database
final List<Resource> resources = findResources();
assertEquals(2, resources.size());
assertTrue(resources.contains(printer));
assertTrue(resources.contains(scanner));
}

....

This test is quite similar, but it uses the unidirectional relation from Team to Resource. Building test data starts on line 6. On line 13 the team with added resources is stored, due to the @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) annotation in Team the resources will be saved together with the team. After this, there must be 2 resources in database, a "Printer" and a "Scanner".

On line 17, the "Printer" gets removed from the team, and the transaction is committed. The test freshly reads the team from database and ensures that is contains only one resource, which must be the "Scanner". Then it asserts that alos the "Printer" still is in database (and thus was not deleted as "orphan").

Conclusion

What would happen when we set orphanRemoval = true onto Team.resources?

@Entity
public class Team extends BaseEntity
{
....

@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, orphanRemoval = true)
private List<Resource> resources = new ArrayList<>();
....
}

Try it out. It would work as expected. Although this is not a bidirectiona hierarchic relation with backlink, the resource would be deleted in database when removed from the team's collection.





ɔ⃝ Fritz Ritzberger, 2020-01-07