영속성 전이(cascade)를 사용할 때 주의해야 할 점
영속성 전이를 프로젝트에 적용하면서 꽤나 많은 우여곡절을 겪었는데, 여기에서 다시 한 번 배운 내용을 정리해보려 합니다.
본 게시글에 사용될 엔티티들의 코드는 다음과 같습니다.
Candidate Entity
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Candidate {
@Id @GeneratedValue
@Column(name = "candidate_id")
private Long id;
private int number;
@Column(name = "candidate_name")
private String name;
@Column(name = "candidate_likes")
private int likes;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "city_id")
private City city;
@OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
private List<Sns> snsList = new ArrayList<>();
@OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
private List<Youtube> youtubeList = new ArrayList<>();
@Builder
public Candidate(int number, String name, City city) {
this.number = number;
this.name = name;
this.likes = 0;
this.city = city;
}
//== 연관관계 편의 메서드 ==//
public void addSns(Sns sns) {
snsList.add(sns);
sns.setCandidate(this);
}
}
Sns Entity
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Sns {
@Id @GeneratedValue
@Column(name = "sns_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "candidate_id")
private Candidate candidate;
@Column(columnDefinition = "Text")
private String content;
private String url;
private LocalDateTime uploadDate;
public Sns(String content, String url, LocalDateTime uploadDate) {
this.content = content;
this.url = url;
this.uploadDate = uploadDate;
}
public void setCandidate(Candidate candidate) {
this.candidate = candidate;
}
}
Facebook Entity
@Entity
@DiscriminatorValue("F")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Facebook extends Sns{
private int likes;
private int comments;
private int shares;
@Builder
public Facebook(String content, String url, LocalDateTime uploadDate, int likes, int comments, int shares) {
super(content, url, uploadDate);
this.likes = likes;
this.comments = comments;
this.shares = shares;
}
public void change(int likes, int comments, int shares) {
this.likes = likes;
this.comments = comments;
this.shares = shares;
}
}
당신의 엔티티, remove()를 수행해도 남아있지 않나요?
EntityManager = em 입니다.
@Test
public void 영속성전이_remove() throws Exception {
//given
Candidate candidate = createCandidate();
Facebook facebook = new Facebook("content1", "url", LocalDateTime.now(), 1, 1, 1);
candidate.addSns(facebook);
candidateRepository.save(candidate);
//when
facebookRepository.remove(facebook); //em.remove(facebook); 과 같은 동작입니다.
//then
Assertions.assertTrue(em.contains(facebook));
}
-
candidate 엔티티의 @OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL) 어노테이션으로 인해, candidate가 persist 되는 시점에 facebook 또한 persist 되는 구조입니다.
-
Entity Manager에서 facebook을 remove() 해주었으니, 테스트는 성공하겠군요?!
성공했습니다!
그러나 쿼리를 날려보면...?
위 코드에서 em.flush()를 추가해서 살펴보겠습니다.
@Test
public void 영속성전이_remove() throws Exception {
//given
Candidate candidate = createCandidate();
Facebook facebook = new Facebook("content1", "url", LocalDateTime.now(), 1, 1, 1);
candidate.addSns(facebook);
candidateRepository.save(candidate);
//when
facebookRepository.remove(facebook);
em.flush();
//then
Assertions.assertTrue(em.contains(facebook));
}
결과
띠용?! 같은 테스트 코드인데, flush()를 해줬다는 이유만으로 실패 테스트로 바뀌었습니다.
여기서 쿼리에 주목해야 하는데, insert 쿼리만 있고 delete 쿼리는 날아가지도 않았습니다.
이유
이유는 CascadeType.all, CascadeType.persist로 설정되어 있기 때문입니다.
EntityManager는 flush(), commit()될 때 영속 상태로 관리되고 있는 엔티티에 대해 쿼리를 날립니다.
- 이 때 EntityManager에 아직 candidate 엔티티가 남아있는데, SnsList에 CascadeType.all 어노테이션이 달려있기 때문에 facebook까지 다시 영속성 컨텍스트에 등록되고, 또 insert 쿼리까지 날아가는 것입니다.
해결책1
candidate의 snsList에서도 facebook 객체를 지워주면 됩니다.
@Test
public void 영속성전이_remove() throws Exception {
//given
Candidate candidate = createCandidate();
Facebook facebook = new Facebook("content1", "url", LocalDateTime.now(), 1, 1, 1);
candidate.addSns(facebook);
candidateRepository.save(candidate);
//when
em.remove(facebook);
candidate.getSnsList().remove(0);//이 부분입니다
em.flush();
//then
Assertions.assertFalse(em.contains(facebook));
}
- 영속성 컨텍스트에서 facebook 객체를 제거해주고
- candidate의 snsList에서도 facebook 객체를 지워주면
- em.flush()에서 쿼리가 나갈 때, 변경 감지 + CascadeType.all에 의해 영속성 컨텍스트에서 facebook 객체가 완전히 분리되면서 delete 쿼리가 나갑니다.
해결책2
candidate의 @OneToMany 어노테이션에 orphanRemoval = true 속성을 넣어주면 됩니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Candidate {
@Id @GeneratedValue
@Column(name = "candidate_id")
private Long id;
private int number;
@Column(name = "candidate_name")
private String name;
@Column(name = "candidate_likes")
private int likes;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "city_id")
private City city;
@OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL, orphanRemoval = true) //바뀐 부분입니다.
private List<Sns> snsList = new ArrayList<>();
@OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
private List<Youtube> youtubeList = new ArrayList<>();
@Builder
public Candidate(int number, String name, City city) {
this.number = number;
this.name = name;
this.likes = 0;
this.city = city;
}
//== 연관관계 편의 메서드 ==//
public void addSns(Sns sns) {
snsList.add(sns);
sns.setCandidate(this);
}
}
Author And Source
이 문제에 관하여(영속성 전이(cascade)를 사용할 때 주의해야 할 점), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@mincho920/영속성-전이cascade를-사용할-때-주의해야-할-점저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)