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).
- Responsibilities cascade all actions, and orphans will be removed, like it is in a hierarchy (UML composition).
-
Resources cascade only on persist (first save) and merge (subsequent update), and orphans will not be removed.
Actually
resources
is not aOneToMany
relation but many-to-many, because there is nomappedBy
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.
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.
Keine Kommentare:
Kommentar veröffentlichen