Cascade
Cascade란 JPA에서 제공하는 영속성 전이라는 기능이다.
영속성 전이는 특정 엔티티에 대해 작업을 수행하면 관련된 엔티티에도 동일한 작업이 수행된다는 의미이다.
일반적으로 작업이 직접 수행되는 엔티티를 부모 엔티티, 부모 엔티티와 관련되어 작업이 간접적으로 수행되는 엔티티를 자식 엔티티라고 부른다.
영속성 전이는 생명주기가 동일한 엔티티들에 적용하면 된다.
또한 자식 엔티티와 연관된 엔티티가 1개일 때만 적용해야한다. 그렇지 않을 경우 자식 엔티티가 다른 엔티티에 필요한 데이터임에도 불구하고, 부모 엔티티가 삭제 될 때 자식 엔티티가 함께 삭제되는 참사가 발생할 수 있다.
영속성 전이의 종류
영속성 전이의 종류에는 ALL, PERSIST, REMOVE, MERGE, DETACH, REFRESH가 있다.
ALL, PERSIST, REMOVE가 주로 사용된다.
- CasCadeType.ALL: 모든 영속성 전이를 적용한다.
- CasCadeType.PERSIST: 부모 엔티티를 영속화 할 때, 자식 엔티티도 함께 영속화한다.
- CasCadeType.REMOVE: 부모 엔티티를 제거할 때, 자식 엔티티도 함께 제거된다.
- CascadeType.MERGE: 부모 엔티티를 병합할 때, 자식 엔티티도 함께 병합된다.
- CascadeType.DETACH: 부모 엔티티를 준영속화 할 때, 자식 엔티티도 함께 준영속화 한다.
- CascadeType.REFRESH: 부모 엔티티를 새로고침 할 때, 자식 엔티티도 함께 새로고침 한다.
영속성 전이 적용 방법
영속성 전이는 @ManyToOne, @OneToMany 어노테이션의 옵션으로 적용할 수 있다.
영속성 전이가 적용된 @ManyToOne, @OneToMany 어노테이션을 필드에 사용하고 있는 엔티티가 부모 엔티티이다!
영속성 전이는 연관관계 매핑, 연관관계의 주인과는 전혀 상관 없는 개발자의 편의를 위한 기능임을 주의하자.
orphanRemoval
orphanRemoval은 JPA가 제공하는 고아객체라는 개념이다.
고아객체란 부모객체와의 관계가 끊어진 자식객체를 의미한다.
고아객체의 의미를 다음과 같이 좀 더 명확하게 정의할 수 있다.
- 부모객체가 삭제된 자식객체
- 부모객체가 가진 컬렉션에서 제외된 자식객체
고아객체가 된 자식객체는 자동으로 삭제된다(DELETE 쿼리 발생).
영속성 전이와 마찬가지로 부모 엔티티와 자식 엔티티의 삭제 주기가 동일한 엔티티들에 적용하면 된다.
또한 자식엔티티와 연관된 엔티티가 1개일 때만 사용해야한다.
고아객체 적용 방법
고아객체는 @OneToMany, @OneToOne 어노테이션에 옵션으로 적용할 수 있다.
고아객체 옵션이 적용된 @OneToMany, @OneToOne 어노테이션을 필드에 사용하고 있는 엔티티가 부모 엔티티이다!
주의할 점은 고아객체가 영속성 전이를 함께 사용하는 경우가 많지만, 항상 함께 적용되야하는 기능이 아니라는 것이다.
사실 영속성 전이와 고아객체 기능을 적용할 수 있는 어노테이션을 살펴보면 알겠지만, 함께 사용 가능한 어노테이션은 @OneToMany 뿐이다.
영속성 전이와 고아객체
CascadeType.ALL와 orphanRemoval = true를 함께 사용한다면, 부모 엔티티의 생명주기와 동일하게 자식 엔티티의 생명주기를 관리할 수 있다.
예시 코드
Parent 엔티티, Child 엔티티가 있다.
Parent 엔티티와 Child 엔티티는 1:N 관계이다.
Parent 엔티티는 Child 객체를 저장하는 childList를 갖고 있다.
그리고 childList에 Cascade.PERSIST, orphanRemoval = true가 적용되어 있다.
즉 Parent 엔티티는 Child 엔티티의 생명주기를 관리하는 부모 엔티티이며, Child는 자식 엔티티이다.
JpaMain 클래스에 실제 실행 코드가 작성되어 있다.
Child 객체 child1, child2는 Parent 객체 parent를 참조한다.
만약 영속성 전이를 사용하지 않았다면,
세 객체를 저장하기 위해 em.persist(parent), em.persist(child1), em.persist(child2)를 각각 호출해야 했을 것이다.
그러나 CascadeType.ALL을 적용했기 때문에, em.persist(parent)만 실행해도 자식 객체인 child1, child2에 대해서도 persist()가 실행된다.
그리고 orphanRemoval = true를 통해 고아객체 기능을 적용했기 때문에,
findParent.childList의 첫 번째 Child 객체를 삭제했을 때, 실제로 child1이 삭제된다.
Parent
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
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 List<Child> getChildList() {
return childList;
}
public void setChildList(List<Child> childList) {
this.childList = childList;
}
}
Child
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
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 Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
JpaMain
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent("parent");
parent.addChild(child1);
parent.addChild(child2);
// 영속성 전이를 적용하지 않을 경우, 각각 persist를 호출해야한다.
// em.persist(parent);
// em.persist(child1);
// em.persist(child2);
// 영속성 전이를 통해 child1, child2도 DB에 저장된다.
em.persist(parent); // DB -> (parent_PK: 1), (child_PK: 1, 2)
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
// parent의 childList에서 첫 번째 child가 제거된다. 첫 번째 child는 고아객체가 되는 것이다.
// 고아 객체는 삭제된다.
findParent.getChildList().remove(0); // DB -> (parent_PK: 1), (child_PK: 2)
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}