orphanRemoval 테스트 문제
https://jyami.tistory.com/22 - 혼자 정리한 자료 : 제일 아랫단
링크 주소
[Parent.java] https://github.com/mjung1798/spring-boot/blob/master/jpa-lab/src/main/java/com/jyami/jpalab/domain/Parent.java
[Child.java] https://github.com/mjung1798/spring-boot/blob/master/jpa-lab/src/main/java/com/jyami/jpalab/domain/Child.java
[테스트코드]https://github.com/mjung1798/spring-boot/blob/master/jpa-lab/src/test/java/com/jyami/jpalab/domain/ParentTest.java
[Parent.java]
@Entity
@NoArgsConstructor
@Getter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
@Builder
public Parent(String name) {
this.name = name;
}
}
[Child.java]
@Entity
@Getter
@NoArgsConstructor
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn
private Parent parent;
@Builder
public Child(String name, Parent parent) {
this.name = name;
this.parent = parent;
parent.getChildList().add(this);
}
}
[테스트 코드]
@Test
public void Parent컬렉션의_child를지웠을_때(){
Parent parent1 = Parent.builder()
.name("parent")
.build();
Child child1 = Child.builder()
.parent(parent1)
.name("child1")
.build();
Child child2 = Child.builder()
.parent(parent1)
.name("child2")
.build();
parentRepository.save(parent1);
childRepository.save(child1);
childRepository.save(child2);
entityManager.clear();
Parent parent = parentRepository.findById(2L).get();
parent.getChildList().remove(0);
parentRepository.save(parent);
entityManager.clear();
Parent findParent = parentRepository.findById(2L).get();
assertThat(findParent.getChildList().size()).isEqualTo(1);
}
안녕하세요? 여기에 답변을 다는게 좋을 것 같아 여기에 달도록 할게요.
일단 먼저 원하시는 답변이 아닐거 같아 죄송합니다. 제가 착각을 했던거 같아요.
저도 김민정님과 동일하게 orphanRemoval = true만으로는 삭제 되지 않네요.
그래서 다시 확인한 결과는 다음과 같습니다.
일단 JPA 스펙상은 위의 코드가 동작을 해야 된다는 스펙인 것 같습니다. 꽤 예전버전(hibernate)이지만 jira에 이슈로 등록 되어 있는게 있는데 위와 같은 현상을 이야기 하는 것 같습니다.
동일하게 orphanRemoval = true만으로는 삭제가 되지 않고 CascadeType.PERSIST 혹은 CascadeType.ALL 을 같이 선언하면 삭제 된다는 내용입니다. 스펙상으로는 orphanRemoval 만으로도 삭제가 되어야 되므로 그 시점엔 수정이 되었던 것 같습니다.
hibernate
4.3.8.Final버전에선 위의 코드가 정상적으로 동작하여 삭제 되는 테스트를 해봤습니다.
그런데 다른 이슈로 인해 해당 코드는 롤백이 되었습니다. OnetoOne 관계일 경우 cascade 없이 자식 엔티티를 영속화 후 플러쉬 할 경우 에러가 발생하여야 하는데 orphanRemoval = true 지정하면 에러 발생하지 않아 롤백한 것으로 추측이 됩니다.
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "parent", orphanRemoval = true)
private Child children;
//..
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
private Parent parent;
//..
}
@Test
public void test() {
entityManager.getTransaction().begin();
Parent parent = new Parent();
Child child = new Child();
parent.setChildren(child);
entityManager.persist(parent);
entityManager.flush(); //error
entityManager.getTransaction().commit();
entityManager.close();
}
대략 적인 위와 같은 코드는 스펙상 에러가 발생 해야 된다고 하지만 orphanRemoval = true 인하여 에러가 발생 하지 않습니다.(hibernate 4.3.8.Final)
해당 부분이 더욱 크리티컬한 부분이라 롤백을 진행한 것으로 추측이 됩니다.
그 이후로는 더 이상 진행 한 것이 없어 보여 저도 알 길이 없네요ㅠㅠ
만약 orphanRemoval 사용할 경우 CascadeType를 같이 사용해야 되는 것 같습니다. (hibernate 경우)
사실 JPA 구현체들은 대부분 해당 스펙에 맞게 개발이 되어있지만 스펙 변경 및 추가가 될 경우 그에 맞게 개발을 진행 못하는 경우도 있을 것 같아 보입니다.
참고로 JPA의 다른 구현제(eclipselink)는 정상적으로 삭제되는 것을 확인했습니다.
원하시는 내용이 아니어서 죄송합니다. 궁금하신거 있으면 언제든지 물어보세요!
덕분에 좋은 부분 공부했습니다. 감사합니다.
@wonwoo 와 친절한 답변 너무 감사합니다! 최근에 행사를 준비하는게 있어서 리뷰를 늦게 봤는데 엄청 알찬내용이네요!! 관련해서 같이 스터디하는 분도 말하시길 이미 Spring-data-JPA로 웬만한건 구현이 가능해서 아무래도 업데이트가 안된 것 같다고 하시더군요
업데이트로 인한 오류였다니 머루님 아니었으면 몰랐을거에요ㅠ 사실 CascadeType.Remove랑 orphanRemoval=true랑 엄청 헷갈렸는데 이제부턴 아예 같이 사용해야한다는 사실도 알려주시고 너무 감사합니다ㅎㅎ
와.. 영속화 후에 플러쉬할 경우라니 생각도 못했는데, 크리티컬하다니 마땅한 업데이트군요 혹시 코드레벨에서 질문을 하자면,
parent.setChildren(child);
entityManager.persist(parent);
entityManager.flush(); //error
여기서 persist(child)를 안했기 때문에, child-parent 사이 관계가 끊어져서 orphanRemoval이 작용했어야 했는데 그게 아니라서 업데이트가 된 건가요??
흠,, 그러면 확실히 OneToOne 관계라 parent 하나만 저장해도 child도 같이 저장되고, parent만 삭제해도 child가 삭제되게 Cascade가 들어가는게 맞는데, orphanRemoval만 사용할 땐 그게 대처가 안되니까..!
제가 이해한 내용이 맞는지도 헷갈리네요 어려워라...
머루님 답변덕에 저도 같이 공부가 되서 답변 너무 감사드립니다 :)
사실 어떻게 보면 orphanRemoval 과 CascadeType 과는 영향이 없어야 된다고 생각합니다.
(현재 Hibernate 버그라고 봐도 될 것 같아요. 하지만 많이 중요하지 않아 수정이 더딜뿐이 아닐까요?)
물론 같이 사용할 때는 블로그 맨 아래 영속성 전이 + 고아 객체, 생명주기에 있듯이 도메인 주도 설계(DDD) 유용하다고 생각됩니다.
하지만 개별적으로 봤을때는 orphanRemoval 의 기능과 CascadeType 기능은 개별적으로 진행되는게 맞는거 같습니다.
orphanRemoval 고아객체 삭제, CascadeType 영속성 전이라 생각하면 될 것 같네요.
또한 블로그의 4.고아객체 의 참고에서도 말햇듯이 해당 상황에선 동일한 기능처럼 동작하곤 합니다.
참고로 조금만 TMI를 히자면 spring + JPA를 처음 접하시는 분들이 오해하는 부분이 있는데 Spring data jpa 프로젝트는 JPA를 좀 더 쉽게(?) 사용할 수 있는 도구일 뿐입니다. 사실상 Spring data 쪽에서 JPA를 직접적으로 제어를 할 수 없습니다. 일종의 쿼리들을 쉽게 날려주는 그런역할? 또는 Spring 과의 좋은 조합?등을 예를들수 있을 것 같습니다.
public interface AccountRepository extends JpaRepository<Account, Long> {
public Account findByName(String name)
@Query("select a from Account a")
public List<Account> getAllAccount()
}
위와 같이 메서드 쿼리나 @Query 어노테이션등을 이용해서 보다 쉽게 쿼리를 할 수 있다는 장점이 있습니다.
그럼 JPA가 무엇인지 알아야 될텐데 조금 쉽게 설명하자면, 사실 JPA는 스펙을 정의한 것이라고 볼 수 있습니다.
현재(JPA 2.2) 대략 600페이지 조금 안되는 문서가 존재하며 그 스펙에 맞게 개발을 해야 됩니다. 궁금하시면 여기를 참고 하시면 됩니다.
그러나 저희는 그것을 개발하기엔 조금 무리가 있으니 무리보다는 그럴필요가 없으니, 일반적으로는 그 스펙대로 개발된 구현체들을 갖다 쓰곤합니다.
예를들어 jboss의 Hibernate, eclipse의 EclipseLink, apache 의 OpenJPA 등 보다 많은 구현체가 있긴한데 이들이 가장 유명하고 많이 사용하고 있는 것 같습니다.
Spring data jpa 역시 기본은 Hibernate 를 사용하고 있지만 원하신다면 다른 구현체로 바꾸실수도 있습니다.
자바 진영에서는 이러한 스펙을 정의하고 표준화시키고 있습니다. 이를 자바스펙요구서(JSR) 이라고 부르며 JPA 역시 JSR에 포함되어 있습니다.
이 외에도 저희들이 자주 사용하고 있는 Servlet, JDBC, DI(Dependency Injection), Validation, Money, JSP 등 대략 200개?(보다 많을 수도 적을수도) 정도가 존재합니다.
이들은 모두 JPA 처럼 대부분 인터페이스만 존재하며 그에 따른 구현체들은 저희가 선택해서 사용할 수 있습니다.
개발적으로 말씀드리면 예를들어 Tomcat(Servlet)을 사용하고 있다가 Jetty(Servlet)로 바꿔도 혹은 또 다른 Servlet 스펙에 맞게 개발만 되어 있다면 어떠한 것을 써도 대부분 문제 없이 사용할 수 있습니다.
이러한 것이 좋은 장점인 것 같습니다.
쓰다보니 꽤 많은 TMI를 했네요. 사실 JPA는 굉장히 런닝커브가 심한편입니다. 저도 아직 많은 부분이 헷갈리곤 합니다. 답변이 제대로 되었는지는 모르겠지만, 공부하는데 도움이 되었으면 좋겠습니다.
감사합니다.
cascade와 orphanRemoval 동작 때문에 이리저리 돌다가 우연찮게 글을 봤는데, 좋은 정보인 것 같네요 감사합니다.
저 같은 경우는 부모 엔티티를 삭제하면, 연관된 자식 엔티티가 삭제되지 않고 자식 엔티티의 참조키값만 null로 바뀌더라구요.
그래서 왜 이런 동작이 발생하나 stackOverFlow에서 찾아본 결과 JPA가 컬렉션을 삭제해야 하는지 여부를 실제로 알지 못한다고 하는데, 아마 위에 말씀처럼 하이버네이트의 버그인거 같구요.
그래서 다른 구현체를 사용하거나 아니면 명시적으로 자식 엔티티를 먼저 지우고 부모 컬렉션에서 제거해야 한다고 합니다.
@wonwoo @Rebwon 답변 감사합니다!!ㅠㅠ 조만간 정리해서 블로그에 글을 올려야겠네요 :) 도움주셔서 감사합니다
https://github.com/Rebwon/ddd-practice/blob/master/src/test/java/ko/maeng/dddpractice/domain/member/MemberTest.java
일대다 다대일 양방향 관계에서 자식 엔티티가 먼저 생성되고 부모 엔티티가 생성되는 과정 안에 자식 엔티티가 추가되면, 부모 엔티티에서 컬렉션인 자식 엔티티를 지우기만 해도 연관된 모든 엔티티가 지워지네요. JPA 관계설정을 하고 insert해주는 부분에서 제대로 넣지 않게되면서 발생한 문제였습니다. 러닝커브가 높은게 이런 이유네요 :)
좋은 내용 공유해주셔서 감사합니다.
현재 저도 동일한 상황을 겪는 중 입니다.
부모 - 자식 관계를 가지는 구조에서 부모에서 자식을 삭제하려고 할 경우
코드
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<MemberImage> images =new ArrayList<>();
//멤버 - 이미지 삭제(고아 객체 자동 삭제)
public void deleteMamberImages(){
int size=images.size();
for(int i=0;i<size;i++)
this.images.remove(0);
}
위와 같이 설정해서 부모에서 자식을 삭제하여 테스트 진행 중인데 DELETE쿼리가 나가지 않습니다.
그런데 cascade = CascadeType.ALL로 수정 후 테스트를 진행하니 DELETE쿼리가 나가 잘 삭제가 되었습니다.
왜 이런 현상이 발생하는지 정확한 원인이 궁금합니다.
@Rebwon 님이 말씀하신것처럼 부모 엔티티가 생성될 때 자식엔티티가 생성되어 자식엔티티에서 연관관계 편의 메서드로 인해 부모의 자식 컬렉션에 추가해서 이런 현상이 발생하는건가요?
아니면 다른 원인이 있는건지 알려주시면 감사하겠습니다