7

Important Notice : If you are reading this post, then consider looking into this post too for in-depth discussions.


It is a quite usual practice/situation/requirement where children of a parent may be migrated to another parent. What happens, if orphanRemoval is set to true on the inverse side of such relationships?

Consider as an example, any simple one-to-many relationship as follows.

Inverse side (Department) :

@OneToMany(mappedBy = "department", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employeeList = new ArrayList<Employee>(0);

Owning side (Employee) :

@JoinColumn(name = "department_id", referencedColumnName = "department_id")
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
private Department department;

While merging an operation/action like the following (where department is a detached entity supplied by a client),

Employee employee = entityManager.find(Employee.class, 1L);
Department newDepartment = entityManager.contains(department) ? department : entityManager.merge(department);

if (!newDepartment.equals(employee.getDepartment())) {
    employee.getDepartment().getEmployeeList().remove(employee);
    // Since orphanRemoval is set to true, 
    // this should cause a row from the database table to be removed inadvertently
    // by issuing an addition DELETE DML statement.
}

employee.setDepartment(newDepartment);
employee.setEmployeeName("xyz");        

List<Employee> employeeList = newDepartment.getEmployeeList();

if (!employeeList.contains(employee)) {
    employeeList.add(employee);
}

entityManager.merge(employee);

Of course, adding and removing an employee may better be done/handled using defensive link (relationship) management methods in the associated entities.

A department instance is supplied by a client. It is a detached entity. It can be the same or a different department depending upon an administrative action performed by the client in question. As a result, if the department instance supplied by a client is different from the one held by the current Employee, then it should be removed first from the list of employees (employeeList) held by the current old department on the inverse side associated with that Employee before adding it to the list of employees held by the new department supplied.

As a guess, the Employee row should be removed inadvertently from the database while removing the Employee instance from the list of employees currently being referred to by the employee's department - old department (before this operation has been triggered) i.e. while migrating a child from its parent to another parent, the child needs to be removed from its native parent before it is adopted by another parent and that child row is supposed to be removed inadvertently from the database (orphanRemoval = true).

The employee row in the database table, however, remains intact with the updated column values. No DML statements except an UPDATE statement are generated.

Can I consider, migrating children from their parent to another parent in this way, does not inadvertently remove those children from the database table as they should not be?

Currently using EclipseLink 2.6.0 having JPA 2.1.


EDIT:

If an Employee entity is only deleted (thus, not added to the list after it was deleted - not migrated to another parent but just deleted) from the list on the inverse side, then its corresponding row is deleted from the database too as usual (orphanRemoval = true) but the row is simply updated, when an Employee entity (child) is added to the list of another parent after it was deleted from the list of its native parent (migration of the entity).

The provider appears to be smart enough to detect, the migration of children from their parent to another parent, as an update.

The behaviour can be seen identical on both Hibernate (4.3.6 final) and EclipseLink (2.6.0) but it cannot be relied upon, if it is a provider specific behaviour (not portable). I cannot find anything about this behaviour in the JPA spec.

Community
  • 1
  • 1
Tiny
  • 27,221
  • 105
  • 339
  • 599

1 Answers1

4

This is documented in the JPA specification.

Section 3.2.4 (excerpt):

The semantics of the flush operation, applied to an entity X are as follows:

  • If X is a managed entity, it is synchronized to the database.
    • For all entities Y referenced by a relationship from X, if the relationship to Y has been annotated with the cascade element value cascade=PERSIST or cascade=ALL, the persist operation is applied to Y

Section 3.2.2 (excerpt):

The semantics of the persist operation, applied to an entity X are as follows:

  • If X is a removed entity, it becomes managed.

orphanRemoval JPA javadoc:

(Optional) Whether to apply the remove operation to entities that have been removed from the relationship and to cascade the remove operation to those entities.

orphanRemoval Hibernate docs:

If an entity is removed from a @OneToMany collection or an associated entity is dereferenced from a @OneToOne association, this associated entity can be marked for deletion if orphanRemoval is set to true.

So, you remove the employee E from the department D1 and add her to the department D2.

Hibernate then synchronizes the department D1 with the database, sees that E is not in the list of employees and marks E for deletion. Then it synchronizes D2 with the database and cascades PERSIST operation to the list of employees (section 3.2.4). Since E is now in this list, the cascade applies on it and Hibernate un-schedules the delete operation (section 3.2.2).

You may want to look at this question as well.

"What happens, if orphanRemoval is set to true on the inverse side of such relationships?"

You have already set it on the inverse side (the inverse side is the one which declares mappedBy). If you mean what if it were set on the other side (@ManyToOne in this case), then it would not make sense and that's why there is no such an attribute in @ManyToOne and @ManyToMany.

Community
  • 1
  • 1
Dragan Bozanovic
  • 23,102
  • 5
  • 43
  • 110
  • I wonder how the specification and the linked question are related to the original post which is about migration of child objects from their parent to another parent. – Tiny Jul 03 '15 at 13:13
  • @Tiny please see my edited answer. Hopefully, I've been more clear now. – Dragan Bozanovic Jul 03 '15 at 14:24
  • Thank you. So that can we conclude that this is a JPA-specific and provider-independent, documented behaviour? Thus, it is not a provider-specific behaviour. (The cascade on the owning side (`Employee`) i.e. `cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH}` is added for decoration only. It makes no difference in behaviour, if removed in its entirely, by the way). – Tiny Jul 03 '15 at 15:51
  • Yes, this is a documented, provider-independent behavior. The difference in behavior would be if you remove cascade from the `employeeList` (or change it to something else other than `ALL` and `PERSIST`). Could you try that? – Dragan Bozanovic Jul 03 '15 at 16:05
  • I gave it a try by removing both `cascade = CascadeType.ALL` and `orphanRemoval = true` on the inverse side of the relationship (`Department`). It made no difference apart from the fact that the `orphanRemoval` feature is disabled (by default). The same `UPDATE` DML statement was issued. (Currently tried on EclipseLink (2.6.0) but hopefully, Hibernate should go in the same way). – Tiny Jul 03 '15 at 16:17
  • No, leave `orphanRemoval`, that's the point. `orphanRemoval` causes deletion of the entity. Without it, an employee that is removed from a department would still exist if you don't add it to another department (or you would get an exception if department_id is not nullable). You would have to manually delete it. – Dragan Bozanovic Jul 03 '15 at 16:53
  • If `cascade = CascadeType.ALL` were to be removed from the inverse side leaving `orphanRemoval = true` as it was, then the corresponding employee row would be updated and then removed from the underlying database table by issuing `UPDATE` and `DELETE` DML statements respectively in sequence. The newly added employee is present in its new department. – Tiny Jul 03 '15 at 16:55
  • Exactly, if `PERSIST` is not cascaded, then the deletion is not cancelled. That confirms what I explained in the answer. – Dragan Bozanovic Jul 03 '15 at 17:09
  • Much gratitude. By the way, I am very much dogmatic in manually awarding a bounty before it eventually quits in seven days :). So, you will have to wait for remaining five days from now onwards. Hopefully, you will cooperate. – Tiny Jul 03 '15 at 18:22
  • yw, I'm glad to be helpful. Sure, no problem, I also have the same attitude towards bounties. – Dragan Bozanovic Jul 03 '15 at 19:50