해결된 질문
작성
·
310
·
수정됨
1
orphanRemoval
을 걸지 않았을 경우
책6을 삭제했으나, userLoanHistories를
조회해보면 여전히 책6이 존재한다.
orphanRemoval = true
을 걸었을 경우
클린 코드를 삭제했더니
조회시 사라졌다
어째서 orphanRemoval
을 걸어주지 않으면
데이터베이스에 반영이 되지 않는건가요?
@Transactional
어노테이션으로 인해 트랜잭션이 시작될 때,
영속성 컨텍스트가 시작되고,
영속성 컨텍스트의 4대 특징 중 하나인
생성/수정 감지로 인해
user
객체의 userLoanHistories
의 값에
변경사항(removeOneHistory
)이 발생하여
자동저장이 될거라고 생각했는데 말이죠..
실제 데이터베이스상에선 userLoanHistories
는
user테이블의 필드가 아니기 때문에,
영속성 컨텍스트가 관리하는 field 에 포함이 되지않는데
orphanRemoval = true
를 걸어주면
비로소 자신이 관리하는 field로 인식하는걸까요?
원리가 너무 궁금해요..
내용추가1)
바로 다음 강의인 책 대출 리팩토링에선
orphanRemoval
같은 옵션을 주지 않고도
new userLoanHistory
를 add한 내용대로 저장이 되는데,
어째서 userLoanHistory
를 remove하는것은
orphanRemoval
옵션이 없으면
데이터의 변경이 일어나지않는지 궁금합니다..
제 생각에는 User
에서 userLoanHistories.add
를 하든 userLoanHistories.remove
를 하든
둘 다 User
와 UserLoanHistory
와의 연관관계를 이용한
데이터를 변경하기위한 접근같은데 ..
add는 되는데 remove는 안되는게 살짝 이해하기 어렵습니다 ㅠ
내용추가2)
returnBook
과 loanBook
, removeOneHistory
의
차이점이 뭘까 고민해봤는데
returnBook
과 loanBook
은
UserLoanHistory
라는 Entity에 접근하는 과정
(loanBook은 Entity객체생성, returnBook은 Entity의
property 수정)이 존재하고,
removeOneHistory
는 Entity에 직접 접근하는게 아니라
단순히 List에 대한 변경일뿐이고, Entity에 대한 직접적인
수정이나 생성이 이뤄진것이 아니므로 데이터베이스의 변경이 일어나지 않았다.
이렇게 이해했는데 제가 이해한 내용이 맞을까요?
생각을 정리하다보니 질문이 길어진 점 죄송합니다 ㅠ
답변 1
1
안녕하세요 영후이님! 정말 좋은 질문 감사드립니다!! 😊
이런 연관관계 관련 옵션이 JPA를 알쏭달쏭 신기방기 어렵게 만드는 원인이 되죠!!
하나씩 풀어서 자세하게 설명드려 보겠습니다! 👍
[1. oprhanRemoval
이 없으면 왜 자식 엔티티가 사라지지를 않는가]
먼저 용어부터 정리해보겠습니다!
1 : N 연관관계에서
1
을 담당하는 Entity를 "부모 엔티티"
N
을 담당하는 Entity를 "자식 엔티티"라고 하겠습니다.
그리고 작성해주신 코드를 확인해보면, removeIf()
를 사용하여, 특정 조건을 만족하는 자식 엔티티를 부모 엔티티에 연결되어 있는 List
에서 제거했습니다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children;
public void removeChild() {
children.removeIf(child -> 조건...);
}
이 상황에서 removeIf
가 되고, @Transactional
로 인해 영속성 컨텍스트가 동작하더라도 orphanRemoval
이 없으면 자식 엔티티는 사리지지 않습니다.
질문 주신 내용의 핵심은 아래와 같습니다!
분명 영속성 컨텍스트의 자동감지 기능을 통해
List
에서Child
가 제거된 것을 알아차릴 것 같은데 왜orphanRemoval
옵션이 없으면 DB에서 자식 엔티티가 사리지지 않을까요?!
그 이유는 자동감지 기능이 동작해서 Child
가 제거된 것을 알아차렸다고 하더라도, 해당 Child
를 DB에서 삭제할 수 없기 때문입니다. 삭제를 하고 싶다면, orphanRemoval
옵션을 추가해야만 하죠.
따라서 orphanRemoval
없이 removeIf
를 사용해서 List에서 자식 엔티티를 제거하더라도 실제 DB에서는 데이터가 제거되지 않습니다.
사실 생각해보면, 단순히 removeIf
만 사용하는 것은 양방향 연관관계를 끊는 것도 아닙니다. 자식 엔티티 쪽에서는 여전히 부모 엔티티를 들고 있기 때문에 (Child 객체는 Parent 객체를 참조하고 있기 때문에) 완전히 연결이 끊긴 것도 아니죠
public class Child {
@ManyToOne
private Parent parent; // 여기에는 parent가 여전히 연결되어 있을겁니다
}
따라서 orphanRemoval
옵션을 다음과 같이 생각하시면 편합니다.
부모가 자식을 일방적으로 끊었을 때도 자식을 DB에서 제거해주는 옵션!
[2. orphanRemoval
없이 자식 엔티티만 제거하고 싶으면 어떻게 해야 할까?]
그렇다면 orphanRemoval
옵션을 사용하지 않고 자식 엔티티만 제거하고 싶다면 어떻게 해야 할까요?
몇 가지 방법이 있겠지만, 가장 간단한 방법으로는 서비스 레이어 자체에서 자식 엔티티를 바로 접근해 제거하는 방법이 있습니다. 예를 들어 아래와 같은 코드를 작성하면 정상적으로 제거가 될거에요!
UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookName(userId, bookName);
userLoanHistoryRepository.delete(history);
[3. orphanRemoval
은 알겠다! 그런데 자식 엔티티의 추가는 왜 동작할까?]
연관관계를 양방향으로 잘 맺으면, 즉 부모도 List에 자식을 갖고 있고, 자식 (연관관계의 주인)도 부모를 참조하고 있으면 부모만 save
를 해도 자식까지 잘 저장되는 것을 확인할 수 있습니다. 이런 현상은 orphanRemoval
이 없어도 왜 정상 동작할까요?
여기서 부모를 저장하더라도 연결된 자식까지 잘 저장되는 이유는 cascade
옵션 덕분입니다. cascade
옵션은 해당 엔티티에 무언가 변화가 있을 때 (ex. 저장) 연관관계를 맺고 있는 엔티티까지 그 변화를 전파하는 옵션입니다.
따라서 다음과 같은 순서로 부모 엔티티와 자식 엔티티가 함께 저장되는거죠
cascade 옵션이 있는 상태로 부모 엔티티와 자식 엔티티를 양방향 연결합니다
이제 부모 엔티티가 저장이 됩니다.
부모 엔티티가 저장이 되면, 그 저장이 자식 엔티티까지 전파되어 연결되어 있는 자식이 저장됩니다.
실제로 만약 cascade
옵션을 제거하시면, 양방향 연결 이후 부모 엔티티를 저장하더라도 자식 엔티티가 저장되지 않는 모습을 확인할 수 있습니다.
결론적으로 이렇게 이해하시면 편하실 것 같아요!
변경 감지는 새로 생기거나 제거된 연관관계를 감지할 수는 있으나 DB에 반영하지는 않는다.
부모에서 자식을 일방적으로 끊은 경우 이를 DB에 반영하고 싶다면 orphanRemoval
옵션을 써야 한다.
부모 엔티티의 변화를 (ex. 저장) 자식 엔티티에게 까지 전파하고 싶다면, cascade
옵션을 써야 한다.
답변이 도움이 되었으면 좋겠습니다. 감사합니다! 🙇 🙏
친절하고 상세한 답변 감사드립니다!!
자식 엔티티 쪽에서는 여전히 부모 엔티티를 들고 있다고 생각하니깐 이해할 수 있었어요 ㅎㅎ
설명 진짜 잘하시는거 같아요
긴 글에도 퀄리티있는 답변 감사합니다!