영속성 전이를 프로젝트에 적용하면서 꽤나 많은 우여곡절을 겪었는데, 여기에서 다시 한 번 배운 내용을 정리해보려 합니다.
본 게시글에 사용될 엔티티들의 코드는 다음과 같습니다.
@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);
}
}
@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;
}
}
@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;
}
}
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()될 때 영속 상태로 관리되고 있는 엔티티에 대해 쿼리를 날립니다.
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));
}
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);
}
}