Cascade REMOVE vs. orphanRemoval = true
Closed this issue · 3 comments
Cascade REMOVE vs. orphanRemoval = true
Cascade REMOVE
Cascade.REMOVE
는 부모 엔티티가 삭제되면 자식 엔티티도 삭제된다. 즉, 부모가 자식의 삭제 생명 주기를 관리한다. 만약 CascadeType.PERSIST
도 함께 사용하면, 부모가 자식의 전체 생명 주기를 관리하게 된다.
해당 옵션의 경우에는 부모 엔티티가 자식 엔티티와의 관계를 제거해도 자식 엔티티는 삭제되지 않고 그대로 남아있다.
Cascade.ALL
= Cascade.PERSIST
+ Cascade.REMOVE
@Entity
@NoArgsConstructor
@Setter @Getter
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
}
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(
mappedBy = "team",
fetch = FetchType.LAZY)
cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
members.add(member);
member.setTeam(this);
}
}
부모 엔티티를 삭제하는 경우
@DataJpaTest
class TeamTest {
@Autowired
private TeamRepository teamRepository;
@Autowired
private MemberRepository memberRepository;
@DisplayName("CascadeType.REMOVE - 부모 엔티티(Team)을 삭제하는 경우")
@Test
void cascadeType_Remove_InCaseOfTeamRemoval() {
// given
Member member1 = new Member();
Member member2 = new Member();
Team team = new Team();
team.addMember(member1);
team.addMember(member2);
teamRepository.save(team);
// when
teamRepository.delete(team);
// then
List<Team> teams = teamRepository.findAll();
List<Member> members = memberRepository.findAll();
assertEquals(0, teams.size());
assertEquals(0, members.size());
}
}
Member에 대한 save를 해주지 않았음에도 불구하고 team에 속한 Member에 대한 insert sql이 실행됨을 볼 수 있다. -> CascadeType.PERSIST
Member에 대한 delete를 해주지 않았음에도 불구하고 team에 속해있던 member에 대한 delete sql이 실행됨을 볼 수 있다. -> CascadeType.REMOVE
부모 엔티티에서 자식 엔티티를 제거하는 경우
@DisplayName("CascadeType.REMOVE - 부모 엔티티(Team)에서 자식 엔티티(Member)를 제거하는 경우")
@Test
void cascadeType_Remove_InCaseOfMemberRemovalFromTeam() {
// given
Member member1 = new Member();
Member member2 = new Member();
Team team = new Team();
team.addMember(member1);
team.addMember(member2);
teamRepository.save(team);
// when
team.getMembers().remove(0);
// then
List<Team> teams = teamRepository.findAll();
List<Member> members = memberRepository.findAll();
assertEquals(1, teams.size());
assertEquals(2, members.size());
}
부모 엔티티(=Team)에서 자식 엔티티(=Member)를 삭제했음에도 불구하고 delete sql이 실행되지 않았다. 영속성 전이 삭제 옵션은 부모와 자식의 관계가 끊어졌다 해서 자식을 삭제하지 않기 때문이다.
orphanRemoval = true
orphanRemoval = true
또한 부모 엔티티가 삭제되면 자식 엔티티도 삭제된다. 따라서 CascadeType.PERSIST
를 함께 사용하면, 이때도 부모가 자식의 전체 생명 주기를 관리하게 된다.
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(
mappedBy = "team",
fetch = FetchType.LAZY,
cascade = CascadeType.PERSIST,
orphanRemoval = true
)
private List<Member> members = new ArrayList<>();
}
부모 엔티티를 삭제하는 경우
@DisplayName("orphanRemoval = true - 부모 엔티티(Team)을 삭제하는 경우")
@Test
void orphanRemoval_True_InCaseOfTeamRemoval() {
// given
Member member1 = new Member();
Member member2 = new Member();
Team team = new Team();
team.addMember(member1);
team.addMember(member2);
teamRepository.save(team);
// when
teamRepository.delete(team);
// then
List<Team> teams = teamRepository.findAll();
List<Member> members = memberRepository.findAll();
assertEquals(0, teams.size());
assertEquals(0, members.size());
}
CascadeType.REMOVE와 마찬가지로 부모 엔티티를 삭제함으로써 연관된 Member에 대한 delete sql이 실행된다.
부모 엔티티에서 자식 엔티티를 제거하는 경우
@DisplayName("orphanRemoval = true - 부모 엔티티(Team)에서 자식 엔티티(Member)를 제거하는 경우")
@Test
void orphanRemoval_True_InCaseOfMemberRemovalFromTeam() {
// given
Member member1 = new Member();
Member member2 = new Member();
Team team = new Team();
team.addMember(member1);
team.addMember(member2);
teamRepository.save(team);
// when
team.getMembers().remove(0);
// then
List<Team> teams = teamRepository.findAll();
List<Member> members = memberRepository.findAll();
assertEquals(1, teams.size());
assertEquals(1, members.size());
}
이전과는 다르게 부모에서 삭제한 자식에 대해서 delete sql이 1번 실행된다. 고아 객체 옵션은 부모와 자식의 관계가 끊어지면 자식을 고아로 취급하고 자식을 삭제하기 때문이다.
비교 결과
-
부모 엔티티 삭제
- CascadeType.REMOVE와 orphanRemoval = true는 부모 엔티티를 삭제하면 자식 엔티티도 삭제한다.
-
부모 엔티티에서 자식 엔티티 제거
- CascadeType.REMOVE는 자식 엔티티가 그대로 남아있는 반면, orphanRemoval = true는 자식 엔티티를 제거한다.
주의점
두 케이스 모두 자식 엔티티에 딱 하나의 부모 엔티티가 연관되어 있는 경우에만 사용해야 한다.
예를 들어 Member(자식)을 Team(부모)도 알고 Parent(부모)도 알고 있다면, CascadeType.REMOVE 또는 orphanRemoval = true를 조심할 필요가 있다. 자식 엔티티를 삭제할 상황이 아닌데도 어느 한쪽의 부모 엔티티를 삭제했거나 부모 엔티티로부터 제거됐다고 자식이 삭제되는 불상사가 일어날 수 있기 때문이다.
저는 완전 반대로 알고 있었네요.
둘다 결국 부모 엔티티가 삭제되면 자식이 삭제되는 것은 동일하나, REMOVE 옵션은 자식과 부모 관계가 끊어져도 삭제는 안되는 것이고 ORPHAN REMOVAL = TRUE는 관계 끊자마자 삭제되어버리는 것이군요,, 저는 둘이 완전 동일한 줄 알았는데 아니였네요 ㅎㅎ 또 하나 배워가네요,, 감사합니다!
좋은 내용 감사합니다! 제가 정확하게 이해했는지 모르겠어서 질문 드려요!
- 부모 엔티티에서 자식 엔티티 제거
이 부분에서 자식엔티티는 부모엔티티와 연관되어있는 '관계?'만 끊어진 것이고 엔티티는 그대로 남아있다? 라고 이해했는데 이게 맞을까요?
좋은 내용 감사합니다! 제가 정확하게 이해했는지 모르겠어서 질문 드려요!
- 부모 엔티티에서 자식 엔티티 제거
이 부분에서 자식엔티티는 부모엔티티와 연관되어있는 '관계?'만 끊어진 것이고 엔티티는 그대로 남아있다? 라고 이해했는데 이게 맞을까요?
CascadeType=REMOVE와 orphanRemoval=True 옵션은 부모와 자식의 생명 주기를 맞추는 역할(=부모가 사라지면 자식을 삭제, 게시판을 삭제하면 게시글을 삭제)을 한다고 생각하는데 위에서 말하고 싶은 부분은 이런 옵션에서 부모를 삭제하는 경우에는 CascadeType=REMOVE, orphanRemoval=True 두 옵션은 동일하게 작동하는데, 부모에서 자식을 삭제하는 경우에는 CascadeType=REMOVE는 작동을 안하고 orphanRemoval=True는 작동을 한다는 부분을 말하고 싶었습니다
그래서 아마 CascadeType=REMOVE만 적용된 상태에서 부모엔티티에서 자식엔티티를 삭제하면 관계(=자식 엔티티의 FK값인 부모의 id값)만 끊어진 것이 아니라 아무런 변화가 없는 것 같네요.