Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merging a managed entity on the @ManyToOne side

Given below a one-to-many relationship from Department to Employee.

Department (parent) :

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

Employee (child) :

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

Merging a managed entity (child) like the following (using CMT in EJB),

Employee employee = entityManager.find(Employee.class, 1L);
employee.setDepartment(department); // department is supplied by a client.
employee.setEmployeeName("xyz");
entityManager.merge(employee);

does not update the corresponding employee row in the database table. It happens only when CascadeType.MERGE is removed from the child @ManyToOne relationship in Employee.

Why doesn't the row in the table get updated? What is the sole purpose of CascadeType.MERGE regarding this example?

I am currently using EclipseLink 2.6.0 having JPA 2.1.

like image 410
Tiny Avatar asked Jun 22 '15 09:06

Tiny


3 Answers

Cascading should always propagate from a Parent to a Child and not the other way around.

In your case, you need to remove the Cascade from the child-side:

@JoinColumn(name = "department_id", referencedColumnName = "department_id")
@ManyToOne(fetch = FetchType.LAZY)
private Department department;

and make sure you set both sides of the association:

Employee employee = entityManager.find(Employee.class, 1L);
employee.setDepartment(department);
employee.setEmployeeName("xyz");
department.getEmployeeList().add(employee);
entityManager.merge(department);
like image 193
Vlad Mihalcea Avatar answered Nov 04 '22 16:11

Vlad Mihalcea


I read other answers and the bugzilla issue as well, but I'm still convinced that this behavior is a bug, or at least a missing feature.

Please follow my thinking:

Employee employee = entityManager.find(Employee.class, 1L);

employee is a managed entity;

employee.setDepartment(department); // department is supplied by a client.

department is a detached entity;

entityManager.merge(employee);

since employee is already managed, merging employee will only trigger merge cascade on department.
So:

merge(department)

Now depatment is managed too; and this will trigger cascade on employees:

merge(employee1)
merge(employee2)
...
merge(employeeN)

but there are two different scenarios:

  1. department.getEmployeeList is already fetched
    it contains detached entities: merge will attach employee# and should not trigger cascade on department, since it has been already called (otherwise generating an infinite cycle).
    Note that employee is not contained in employeeList.
    In this case everything has to work.
  2. department.getEmployeeList is not yet fetched and is lazy loaded now
    it contains managed entities: merge should do nothing since employees# are already managed and cascade on department has been already called.
    But..

In this case, is employee contained in employeeList?

Three possibilities (depending on other variables like flush-mode, auto-commit, ...):

  1. no: everything should work
  2. yes, and it's the same instance: everything should work
  3. yes, but it's a different instance: probably leads to changes overwrite

I think the "bug" could be here, when lazy loading: it should not be possible to have two managed instances of the same entity in the same EntityManager.

I can't think a single counterexample.

like image 34
Michele Mariotti Avatar answered Nov 04 '22 17:11

Michele Mariotti


This code is risky, as you are associating a detached department instance, which then presumably has a detached collection of employees. If your current employee with the new xyz name is in that list, then its changes will get overridden by the detached instance's name.

for example, after you call employee.setDepartment(department); employee(1L) -> department' -> employee(1L)'

Calling merge on the employee(1L) instance will do nothing as the name change is already visible, but it will casacade to the department. Merging department then cascades to the employee(1L)' instance which has the old name. If you checked the value of the employee.getEmployeeName(), you would see that merge caused it to reset, which is likely why you do not see a database update.

Not calling merge though is not an option, because you still have employee referencing a detached department, which is supposed to be an exception case. This is why the provider issues inserts.

Presumably you have cascade merge set on the relationships because the department object supplied by the client could contain employee changes as well. If so, to synchronize these changes and change the employeeName, you would use:

Department managedDepartment = entityManager.merge(department);
Employee employee = entityManager.find(Employee.class, 1L);
employee.setDepartment(managedDepartment);
managedDepartment.getEmployeeList().add(employee);
employee.setEmployeeName("xyz");

This both can add the employee to the department if it isn't there, and still make the name change if it is.

like image 1
Chris Avatar answered Nov 04 '22 15:11

Chris