Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cascade REMOVE vs. orphanRemoval = true #100

Closed
yoon-youngjin opened this issue May 16, 2022 · 3 comments
Closed

Cascade REMOVE vs. orphanRemoval = true #100

yoon-youngjin opened this issue May 16, 2022 · 3 comments

Comments

@yoon-youngjin
Copy link
Collaborator

yoon-youngjin commented May 16, 2022

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());
    }
}

image

Member에 대한 save를 해주지 않았음에도 불구하고 team에 속한 Member에 대한 insert sql이 실행됨을 볼 수 있다. -> CascadeType.PERSIST

image

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());
    }

image

부모 엔티티(=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());
    }

image

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());
    }

image

이전과는 다르게 부모에서 삭제한 자식에 대해서 delete sql이 1번 실행된다. 고아 객체 옵션은 부모와 자식의 관계가 끊어지면 자식을 고아로 취급하고 자식을 삭제하기 때문이다.

비교 결과

  • 부모 엔티티 삭제

    • CascadeType.REMOVE와 orphanRemoval = true는 부모 엔티티를 삭제하면 자식 엔티티도 삭제한다.
  • 부모 엔티티에서 자식 엔티티 제거

    • CascadeType.REMOVE는 자식 엔티티가 그대로 남아있는 반면, orphanRemoval = true는 자식 엔티티를 제거한다.

주의점

두 케이스 모두 자식 엔티티에 딱 하나의 부모 엔티티가 연관되어 있는 경우에만 사용해야 한다.

예를 들어 Member(자식)을 Team(부모)도 알고 Parent(부모)도 알고 있다면, CascadeType.REMOVE 또는 orphanRemoval = true를 조심할 필요가 있다. 자식 엔티티를 삭제할 상황이 아닌데도 어느 한쪽의 부모 엔티티를 삭제했거나 부모 엔티티로부터 제거됐다고 자식이 삭제되는 불상사가 일어날 수 있기 때문이다.


저는 완전 반대로 알고 있었네요.

@myway00
Copy link
Contributor

myway00 commented May 18, 2022

둘다 결국 부모 엔티티가 삭제되면 자식이 삭제되는 것은 동일하나, REMOVE 옵션은 자식과 부모 관계가 끊어져도 삭제는 안되는 것이고 ORPHAN REMOVAL = TRUE는 관계 끊자마자 삭제되어버리는 것이군요,, 저는 둘이 완전 동일한 줄 알았는데 아니였네요 ㅎㅎ 또 하나 배워가네요,, 감사합니다!

@hehahihang
Copy link
Collaborator

hehahihang commented May 18, 2022

좋은 내용 감사합니다! 제가 정확하게 이해했는지 모르겠어서 질문 드려요!

  • 부모 엔티티에서 자식 엔티티 제거
    이 부분에서 자식엔티티는 부모엔티티와 연관되어있는 '관계?'만 끊어진 것이고 엔티티는 그대로 남아있다? 라고 이해했는데 이게 맞을까요?

@yoon-youngjin
Copy link
Collaborator Author

yoon-youngjin commented May 19, 2022

좋은 내용 감사합니다! 제가 정확하게 이해했는지 모르겠어서 질문 드려요!

  • 부모 엔티티에서 자식 엔티티 제거
    이 부분에서 자식엔티티는 부모엔티티와 연관되어있는 '관계?'만 끊어진 것이고 엔티티는 그대로 남아있다? 라고 이해했는데 이게 맞을까요?

CascadeType=REMOVE와 orphanRemoval=True 옵션은 부모와 자식의 생명 주기를 맞추는 역할(=부모가 사라지면 자식을 삭제, 게시판을 삭제하면 게시글을 삭제)을 한다고 생각하는데 위에서 말하고 싶은 부분은 이런 옵션에서 부모를 삭제하는 경우에는 CascadeType=REMOVE, orphanRemoval=True 두 옵션은 동일하게 작동하는데, 부모에서 자식을 삭제하는 경우에는 CascadeType=REMOVE는 작동을 안하고 orphanRemoval=True는 작동을 한다는 부분을 말하고 싶었습니다

그래서 아마 CascadeType=REMOVE만 적용된 상태에서 부모엔티티에서 자식엔티티를 삭제하면 관계(=자식 엔티티의 FK값인 부모의 id값)만 끊어진 것이 아니라 아무런 변화가 없는 것 같네요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants