[JPA] JPA에서 On Delete Cascade 제약조건 사용하기
spring.jpa.hibernate.ddl-auto=[create|update|create-drop|validate|none]
위와 같이 Hibernate에서 제공해주는 DDL 자동 생성 기능을 통해, 개발 단계에서 테이블을 간편하게 생성할 수 있다.
(비록 운영 단계에서는 validate 혹은 웬만하면 none 옵션을 사용하지만...)
하지만 부모테이블을 참조하는 자식 테이블이 있을 경우, 부모 테이블의 레코드가 삭제될 시에 해당 레코드를 참조하는 자식 테이블의 모든 레코드를 연쇄적으로 삭제하고 싶을 경우가 생길 수 있다.
예를 들어 SuccessCase라는 성공사례 게시판을 나타내는 부모 테이블이 있고, 이 게시글의 PK를 참조하는 Comment라는 댓글 테이블이 있다고 해보자.
게시글이 사라지는 순간 해당 게시글에 달린 모든 댓글들은 사실상 의미가 없다고 볼 수 있다.
그렇기에 'on delete cascade'라는 무결성 제약조건(참조 무결성)을 추가하여, 부모 테이블의 레코드가 삭제될 때 연관관계가 있는 자식 테이블의 레코드를 연쇄적으로 삭제하는 방법이 있다.
JPA를 사용하는 환경에서 이를 구현하기 위해서는 3가지 정도의 방법이 있는데, 그 방법들은 다음과 같다:
- 비즈니스 로직을 수행할 Service 단에서 SuccessCase의 레코드를 삭제하기에 앞서, 해당 레코드와 연관관계가 있는 Comment 타입의 instance들을 모두 명시적으로 삭제하는 방법
- 관계가 있는 두 엔터티들을 양방향 관계로 매핑하는 방법
- 단방향 매핑을 유지하면서, @OnDelete 어노테이션을 사용하는 방법
이 중에서 2번째 방법은 각 엔터티에서 추가적인 메서드들을 구현해줘야 하는 번거로움이 있다.
@Entity
@Table(name = "success_case")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SuccessCase extends BaseTime {
@Id
@Column(name = "case_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer caseId;
... // 기타 속성들 중략
@OneToMany(mappedBy = "successCase", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
... // 일대다 관계의 편의성을 보장하기 위해서 추가적인 메서드 구현 필요
}
@Entity
@Table(name = "comment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseTime {
@Id
@Column(name = "comment_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer commentId;
... // 기타 속성들 중략
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "case_id", nullable = false)
private SuccessCase successCase;
// 대댓글과의 양방향 매핑
@OneToMany(mappedBy = "comment", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
private List<Reply> replies = new ArrayList<>();
... // 일대다 관계의 편의성을 보장하기 위해서 추가적인 메서드 구현 필요
}
이처럼 엔터티 구성 후에 List 안에 자식 테이블의 레코드를 추가 혹은 변경하기 위한 추가적인 메서드를 구현해야 한다.
이러한 번거로움을 해소할 수 있는 방법이 위에서 언급했던 3번에 해당하는 방법이다. 이 방법은 @OnDelete라는 어노테이션 하나만 추가해 줌으로서, 1번과 2번 방법을 모두 생략이 가능하다.
사용방법은 비교적 간단하다. 자식 엔터티에서 부모 엔터티를 참조하는 외래키에 해당하는 컬럼에 @OnDelete라는 어노테이션을 추가해주면 된다.
@Entity
@Table(name = "success_case")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SuccessCase extends BaseTime {
@Id
@Column(name = "case_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer caseId;
... // 기타 속성들 중략
// 일대다 관계의 편의성을 보장하기 위해서 추가적인 메서드 구현 필요 없음
}
@Entity
@Table(name = "comment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseTime {
@Id
@Column(name = "comment_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer commentId;
... // 기타 속성들 중략
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "case_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private SuccessCase successCase;
// 일대다 관계의 편의성을 보장하기 위해서 추가적인 메서드 구현 필요 없음
}
위의 코드를 통해 부모와 자식 간의 관계를 단반향으로 바꾸었다. 그리고 Comment 엔터티에서 부모 엔터티를 참조하는 successCase 필드에 @OnDelete 어노테이션과 함께 action을 추가해 줌으로서, 'on delete cascade' 제약조건을 걸어주게 되었다. 이렇게 하게되면 SuccessCase의 특정 레코드가 삭제될 때 마다, 해당 레코드와 관계가 있는(해당 레코드를 참조하고 있는) 모든 자식 레코드가 연쇄적으로 삭제되게 된다.
정말 간편하고 실용적인 만큼 꼭 알아두면 도움이 될 것 같다!