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.
Following UML class diagram shows example relations so that we can understand orphans.
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.
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.
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!
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).
resources
is not a OneToMany
relation but many-to-many, because there is no mappedBy
attribute. @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.
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 | public abstract class JpaTest |
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.
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 | .... |
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").
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