merge 시 orphanRemoval이 작동할까? (feat. 하이버네이트 버그)

유알·2024년 4월 19일
0

프로젝트를 하다가 헷갈리는 점에 대해 테스트 코드를 작성해 보았다.

준영속 상태의 엔티티를 영속 상태로 만들기 위해 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 가 없으면 작동하지 않는다.

profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글