부모 엔티티를 저장할 때 연관된 자식 엔티티도 함께 저장하고 싶다면 다음과 같은 과정을 거쳐야 한다.
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent);
Parent
는 List<Child>
필드를 가지고 있다.
연관관계 매핑은 Child
를 연관관계의 주인으로 설정하여 다대일 양방향 연관관계를 매핑했다.
위와 같이 Parent
를 영속한다고, Parent
가 소유하고 있는 Child
는 영속되지 않는다.
Child
엔티티를 영속하려면 명시적으로 Child
엔티티를 영속시켜야 한다.
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child); // 아무런 기능도 하지 않는다...
// 주의! 여기서 순서가 바뀌면 UPDATE 쿼리가 추가로 발생한다!
parentRepository.save(parent);
childRepository.save(child);
Parent
의 addChild()
메소드는 아무런 기능도 하지 않는다.
해당 메소드는 비즈니스 로직 상 어떠한 유의미한 기능을 위해 사용될 수 있겠지만, 관련된 엔티티를 같이 영속하는 기능은 제공하지 못한다.
하지만 사람이 보기엔 Parent
엔티티가 영속될 때, 소유한 Child
엔티티도 같이 영속되는 것이 자연스럽다.
어떻게 하면 Parent
엔티티를 영속할 때, 소유한 Child
엔티티도 함께 영속시킬 수 있을까?
Cascade를 사용하면 이것이 가능해진다.
다음과 같이 연관관계 매핑 어노테이션의 속성으로 cascade
를 설정하면 된다.
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
...
}
cascade 옵션은 일대다 관계뿐 아니라, 다대일 관계에서도 설정할 수 있다.
그리고 다음과 같이 Parent
를 영속할 때, Child
도 함께 영속할 수 있다.
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent); // Parent가 소유한 Child도 영속된다.
단순히 영속할 때 말고, 영속 상태가 변경될 때도 cascade
를 사용하면 편하게 영속 상태를 관리할 수 있다.
CascadeType
은 열거형으로, 다음과 같은 열거 타입이 있다.
ALL
, PERSIST
, MERGE
, REMOVE
, REFRESH
, DETACH
ALL
은 말 그대로 모든 상태에 대해 작동한다.
나머지는 해당 영속 상태에 작동한다.
주로 사용되는 것은 PERSIST
와 REMOVE
2가지이다.
Parent
엔티티를 삭제할 때, 연관된 Child
도 함께 삭제하려면 CascadeType.REMOVE
를 추가하면 된다.
@OneToMany(mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<Child> children = new ArrayList<>();
그리고 다음과 같이 Parent
엔티티를 삭제하면 연관된 Child
엔티티도 삭제된다.
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent);
// flush
parentRepository.delete(parent);
즉, Cascade 설정으로 자식 엔티티의 생명 주기를 부모 엔티티가 관리하는 것이 가능하다.
그렇다면 여기서 Parent
의 List<Child>
필드에서 특정 Child
를 제거한다면 해당 Child
엔티티가 삭제될 것이라 기대할 수 있다.
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent);
childRepository.save(child);
// flush
parent.removeChild(child); // parent.getChildren().remove(child);
하지만 기대와 달리, 해당 Child
엔티티는 삭제되지 않는다.
왜냐하면 Cascade 옵션은 자신의 영속 상태를 전파해 주기 때문에, 연관관계를 맺은 엔티티의 참조가 없어지더라도 영속 상태가 반영되지 않는다.
이때 orphanRemoval
속성을 사용하면 기대하는 기능을 누릴 수 있다.
다음과 같이 연관관계 매핑 어노테이션의 속성으로 orphanRemoval
를 설정하면 된다.
@OneToMany(mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
그리고 Parent
의 List<Child>
필드에서 특정 Child
를 제거하면 해당 Child
엔티티가 삭제된다.
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent);
childRepository.save(child);
// flush
parent.removeChild(child); // DELETE 쿼리 발생
Cascade와 OrphanRemoval 옵션은 주로 같이 사용하는데, 이때 주의할 점이 있다.
orphanRemoval
옵션을 사용할 때, CascadeType.REMOVE
를 사용하지 않아도 같은 효과가 적용된다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent);
// flush
parentRepository.delete(parent); // Child도 함께 DELETE 된다.
어찌 보면 당연한 결과라고 생각할 수 있지만, Parent
가 삭제되더라도, Child
는 삭제되지 않길 원할 때 예상하지 못한 상황이 생길 수 있으므로 주의해야 한다.
또한, CascadeType.PERSIST
속성을 사용하지 않고 orphanRemoval
속성을 사용하면 orphanRemoval
기능이 작동하지 않는다.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
Parent parent = new Parent();
Child child = new Child(parent);
parent.addChild(child);
parentRepository.save(parent);
childRepository.save(child);
em.flush();
parent.remove(child); // DELETE 쿼리가 발생하지 않는다.
해당 이슈는 JPA의 구현체인 하이버네이트의 버그이기 때문에 CascadeType.PERSIST
가 적용되어야 동작한다고 한다.