프로젝트를 하다가 헷갈리는 점에 대해 테스트 코드를 작성해 보았다.
준영속 상태의 엔티티를 영속 상태로 만들기 위해 entityManager#merge()
가 사용된다.
주의해야할 점은 준영속 상태의 엔티티의 값들이 모두 덮어 씌워지는 것이다.
근데 헷갈리는 점이 있었는데, 그렇다면, 이렇게 덮어 씌워질 때, orphanRemoval이 작동할까? 라는 점이었다.
package jpa_test.merge_orphan_removal;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "department", cascade = CascadeType.PERSIST,orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
public Long getId() {
return id;
}
public List<Employee> getEmployees() {
return employees;
}
public void setId(Long id) {
this.id = id;
}
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
}
package jpa_test.merge_orphan_removal;
import javax.persistence.*;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
@Test
void orphanRemovalTest() {
// 미리 저장해놓는다.
Department department = new Department();
System.out.println("em.persist(department);");
em.persist(department);
Employee employee = new Employee();
employee.setDepartment(department);
employee.setName("이름");
department.getEmployees().add(employee);
System.out.println("em.persist(employee);");
em.persist(employee);
// id를 미리 알아 놓는다.
Long employeeId = employee.getId();
Long departmentId = department.getId();
// 영속성 컨텍스트를 초기화한다.
System.out.println("em.flush();");
em.flush();
em.clear();
// department 를 만들어서 merge 한다.
Department detachedDepartment = new Department();
detachedDepartment.setId(departmentId);
System.out.println("em.merge(detachedDepartment);");
Department merged = em.merge(detachedDepartment);
System.out.println("em.persist(merged);" + merged.getEmployees().size());
em.persist(merged);// 여기서 과연 orphanRemoval이 동작할까?
// 동작한다
Assertions.assertEquals(0, merged.getEmployees().size());
Assertions.assertEquals(0, em.find(Department.class, departmentId).getEmployees().size());
// 영속성 컨텍스트를 초기화한다.
System.out.println("em.flush();");
em.flush();
System.out.println("em.clear();");
em.clear();
System.out.println("em.find(Employee.class, " + employeeId + ");");
Employee findedEmployee = em.find(Employee.class, employeeId);// 여기서 department가 null이 아닌지 확인한다.
System.out.println("em.find(Department.class, " + departmentId + ");");
Department findedDepartment = em.find(Department.class, departmentId);
// 다시 조회해도 잘 지워져 있다.
Assertions.assertNull(findedEmployee);
Assertions.assertEquals(0, findedDepartment.getEmployees().size());
}
em.persist(department);
Hibernate:
insert
into
Department
(id)
values
(null)
em.persist(employee);
Hibernate:
insert
into
Employee
(id, department_id, name)
values
(null, ?, ?)
em.flush();
em.merge(detachedDepartment);
Hibernate:
select
department0_.id as id1_1_0_
from
Department department0_
where
department0_.id=?
Hibernate:
select
employees0_.department_id as departme3_2_0_,
employees0_.id as id1_2_0_,
employees0_.id as id1_2_1_,
employees0_.department_id as departme3_2_1_,
employees0_.name as name2_2_1_
from
Employee employees0_
where
employees0_.department_id=?
em.persist(merged);0
em.flush();
Hibernate:
delete
from
Employee
where
id=?
em.clear();
em.find(Employee.class, 1);
Hibernate:
select
employee0_.id as id1_2_0_,
employee0_.department_id as departme3_2_0_,
employee0_.name as name2_2_0_,
department1_.id as id1_1_1_
from
Employee employee0_
left outer join
Department department1_
on employee0_.department_id=department1_.id
where
employee0_.id=?
em.find(Department.class, 1);
Hibernate:
select
department0_.id as id1_1_0_
from
Department department0_
where
department0_.id=?
Hibernate:
select
employees0_.department_id as departme3_2_0_,
employees0_.id as id1_2_0_,
employees0_.id as id1_2_1_,
employees0_.department_id as departme3_2_1_,
employees0_.name as name2_2_1_
from
Employee employees0_
where
employees0_.department_id=?
결과적으로 보면, merge를 통해 리스트가 덮어 씌워지더라도 마치 유저가 list를 clear 한거 마냥 고아가 제거된다.
당연히 제거되지 않을까 하고 테스트를 돌렸지만, orphanRemoval 이 아예 작동을 하지 않았다. 그래서 더 찾아보니까, 하이버네이트에 버그가 있다고 한다.
JPA 스펙상에는 Cascade.PERSIST가 없이도 orphanRemoval이 돌아가야하지만, 하이버네이트 구현체의 경우 Cascade.PERSIST 가 없으면 작동하지 않는다.