이전에 Tree구조로 구성된 카테고리를 db를 구현하였다.

이 구조에서 delete기능을 수행시에 전체 데이터가 한번에 삭제되는 문제가 있었다.

  • 도메인
public class MenuCategory {

	...
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "parentId")
    @JsonIgnore
    private MenuCategory parent;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "parentId")
    private List<MenuCategory> children = new ArrayList<>();
    
    ...
}
  • import.sql을 통해 19개의 트리구조 데이터를 입력
INSERT INTO menu_category (name, parent_id, id) values ('categories', null , 19l)
INSERT INTO menu_category (name, parent_id, id) values ('밑반찬', 19l, 1l);
INSERT INTO menu_category (name, parent_id, id) values ('무침', 1l, 2l);
INSERT INTO menu_category (name, parent_id, id) values ('나물무침', 1l, 3l);
INSERT INTO menu_category (name, parent_id, id) values ('볶음', 1l, 4l);
INSERT INTO menu_category (name, parent_id, id) values ('조림', 1l, 5l);

INSERT INTO menu_category (name, parent_id, id) values ('국·찌개', 19l, 6l);
INSERT INTO menu_category (name, parent_id, id) values ('담백한국', 6l, 7l);
INSERT INTO menu_category (name, parent_id, id) values ('찌개', 6l, 8l);

...

테스트 파일

{
    ...
        
	@Test
    public void delete_operate_test() {
        MenuCategory fstCategory = categoryRepository.findById(1l).get();
        categoryRepository.delete(fstCategory);

        log.info("남은 카테고리 : '{}'", categoryRepository.findAll());
        assertThat(categoryRepository.findAll().size()).isEqualTo(18);

    }

	...
}

결과

image-20190419103535633

image-20190419103752558

설명

  • 위와 같은식으로 delete쿼리가 연속적으로 발생해서 모든 데이터를 삭제한다. ( 직계자식 -> 형제 -> 부모 순으로 delete쿼리가 발생한다. )
  • 삭제순서 확인을위해 level이 4인 자식의 자식을 입력해 보았더니 이 노드의 부모를 삭제하기 전에 자식을 먼저 삭제하고 부모를 삭제하는것을 확인하였다.
  • 쿼리의 삭제순서는 현재 노드 기준으로 자식 -> 본인 -> 부모 순이다. 이때 자식(1)에게 또 자식(2)이 있다면, 그 자식(2)을 삭제하고 나서 자식(1)을 삭제 한다. 논리적으로 일관된 순서를 보여준다는 점에 감탄.
  • 또한 본인 삭제후 부모를 삭제할때 부모의 자식(현재 노드 기준으로는 형제)이 더있다면 자식을 모두 삭제한 후에 부모노드를 삭제 한다.

Cascade vs Fetch

1. 첫번째 가정

현재 문제는 cascadeType.remove로 인해 연관된 엔티티를 삭제하는데, 이때 @ManyToOne칼럼의 default FetchType인 EAGER로 인해 가져와진 parent를 삭제함으로써 발생한 일이라고 판단된다.

결과

@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "parentId")
    @JsonIgnore
    private MenuCategory parent;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "parentId")
    private List<MenuCategory> children = new ArrayList<>();

image-20190419105127854

잘못된 가정이었음을 알 수 있었다. 생각해보면 위의 가정은 sibling을 삭제하는 이유는 설명할 수 있지만, children을 삭제하는 이유는 설명 할 수 없다. 그렇다면 CascadType가 원인이라고 판단할 수 있다.

2. 두번째 가정

부모에게 설정된 Cascade.remove를 삭제하면 부모도 delete쿼리가 올라가는 일이 없을것이다.

image-20190419112210199

결과

image-20190419112337629

부모는 삭제하지 않고, 자식과 자식의 자식들만 삭제하는 모습을 부여준다

결론

부모 엔티티에 CascadeType 속성을 걸면 repository method가 부모를 타고 올라간다는 너무 당연한 점을 재학습하였다.