[JPA] Cascade PERSIST, REMOVE
Cascade
Cascade : 종속
한 객체가 영속성 상태일 때 연관된 엔티티도 함께 영속성 상태로 만들고 싶다면 영속성 전이를 사용한다. JPA는 cascade
옵션으로 영속성 전이를 사용할 수 있다.
땡쿠팀의 도메인
현재 땡쿠팀은 cascade
를 한 곳에서 사용하고 있습니다.
Meeting
과 Member
가 다대다 관계이고 그 사이에 중간 MeetingMember
라는 중간 테이블이 존재합니다.
연관 관계
서비스적인 구조를 살펴보면 Meeting
이 생성되기 위해선 반드시 서비스에 가입한 Member
가 먼저 저장되어 있어야 합니다.
따라서 Meeting
은 Member
가 누군지 알고, 검증해야 합니다. 반대로 Member
는 Meeting
이라는 존재를 몰라도 됩니다.
이러면 의존방향이 Meeting
→ Member
단방향으로 흐르게 됩니다.
따라서 Meeting
을 통해 MeetingMember
를 생성하도록 구현했습니다.
Meeting 객체가 스스로 MeetingMembers를 생성합니다.
생성된 MeettingMember 객체는 Transient 상태기 때문에 Meeting 저장될 때 함께 영속상태가 되어야 합니다. 따라서 CascadeType.PERSIST
옵션을 사용했습니다.
@OneToMany(mappedBy = "meeting", cascade = **CascadeType.PERSIST**, orphanRemoval = true)
private List<MeetingMember> meetingMembers = new ArrayList<>();
Cascade.PERSIST
CascadeType.PERSIST
: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지
@Test
void persist() {
Member sender = memberRepository.save(Member.builder().name("호호").build());
Member receiver = memberRepository.save(Member.builder().name("후니").build());
Coupon coupon = 쿠폰_저장(sender, receiver);
// 현재 PERSIST 옵션
Meeting meeting = meetingRepository.save(Meeting.builder()
.coupon(coupon)
.members(List.of(sender, receiver))
.meetingStatus(ON_PROGRESS)
.meetingTime(LocalDate.now())
.build());
}
MeetingMember
는 영속 상태가 아니다. 미팅 객체가 생성되는 시점에 생성자안에서 생성되는 객체이지만 PERSIST
옵션 덕분에 비영속 객체도 영속 객체로 전이를 시켰다.
cascade 옵션을 사용하지 않는다면 meetingMember는 insert 되지 않는다.
Cascade.REMOVE
CascadeType.REMOVE
: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거
땡쿠팀은 아직 삭제 기능이 존재하지 않는다. 앞으로 삭제 기능은 생기게 될텐데 정책에 맞춰 어디까지 삭제를 해야할지 고민해야 한다.
- 회원이 탈퇴할 경우 현재 예약, 쿠폰, 미팅을 모두 삭제해야 하는지
- 쿠폰 삭제가 정책상 필요한건지
우선 몇가지 경우의 수로 예제를 만들어보자.
회원이 탈퇴할 경우 관련된 모든 것을 삭제하겠다는 정책이 나온경우.
@Test
void deleteMember() {
Member sender = memberRepository.save(Member.builder().name("호호").build());
Member receiver = memberRepository.save(Member.builder().name("후니").build());
Coupon coupon = 쿠폰_저장(sender, receiver);
Meeting meeting = meetingRepository.save(Meeting.builder()
.coupon(coupon)
.members(List.of(sender, receiver))
.meetingStatus(ON_PROGRESS)
.meetingTime(LocalDate.now())
.build());
memberRepository.deleteById(sender.getId()); // 예외 발생
}
미팅 멤버와 연관된 회원때문에 삭제하지 못 한다.
삭제하기 위해선 미팅을 먼저 삭제하고 미팅 멤버를 삭제한 뒤 회원을 삭제해야 한다. 쿠폰의 경우 간접참조를 하고 있어서 이번 예제에서는 제외하겠다. 방법은 2가지가 있을 것 같다.
- 미팅을 삭제하기 전 미팅 멤버를 모두 삭제 한 뒤 미팅을 삭제 한다.
그러기 위해선 MeetingMeberRepository도 필요해진다.
- Cascade의 REMOVE 옵션을 사용하여 자식 객체도 함께 삭제한다.
Member sender = memberRepository.save(Member.builder().name("호호").build());
Member receiver = memberRepository.save(Member.builder().name("후니").build());
Coupon coupon = 쿠폰_저장(sender, receiver);
Meeting meeting = meetingRepository.save(Meeting.builder()
.coupon(coupon)
.members(List.of(sender, receiver))
.meetingStatus(ON_PROGRESS)
.meetingTime(LocalDate.now())
.build());
meetingRepository.delete(meeting);
memberRepository.delete(sender);
- 부모 엔티티 삭제
CascadeType.REMOVE
와orphanRemoval = true
는 부모 엔티티를 삭제하면 자식 엔티티도 삭제한다.
- 부모 엔티티에서 자식 엔티티 제거
CascadeType.REMOVE
는 자식 엔티티가 그대로 남아있는 반면,orphanRemoval = true
는 자식 엔티티를 제거한다.