처음 생각: 삭제하기 위한 Candidate를 id로 조회할 때, 관련 엔티티(sns, youtube)들을 함께 fetch 조인해야 cascade가 잘 적용되겠지?
그래서 join fetch 쿼리를 작성하던 중 김영한님의 인프런 강의에서 들었던 내용이 생각났습니다.
- 둘 이상의 컬렉션은 페치 조인하면 안된다
- @ToMany 페치 조인에서는 페이징을 사용할 수 없다
또한 Total likes, Total comments 등 통계 기능은 Sns, Youtube 테이블을 모두 조인해서 결과를 내야하는데... 강의 내용에 따르면
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
현재 Sns, Youtube 엔티티는 Candidate에 연관관계가 걸려있기 때문에 Spring Boot 시작 시 다음과 같은 쿼리가 날아갑니다.
이제 Candidate에서 Cascade로 관리하지 않기 때문에, candidateRepository.remove(candidate)를 시도하면 다음과 같은 오류가 발생합니다.
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["FKDGYKRP9VG5PBS7S05I53938T2: PUBLIC.SNS FOREIGN KEY(CANDIDATE_ID) REFERENCES PUBLIC.CANDIDATE(CANDIDATE_ID) (2)"; SQL statement:
delete from candidate where candidate_id=? [23503-200]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
즉, 외래 키 무결성 요건이 위배되기 때문에 쿼리 실행 자체가 막히게 됩니다. 당연한 일입니다. 스프링에서 외래 키 무결성을 관리하지 않으니까요.
따라서 이 경우에는 DB에서 외래 키 무결성을 관리하도록 설정을 바꿔주어야 합니다.
SQL에서는 ON DELETE CASCADE로 DB에서 설정하지만, 우리는 Spring Boot로 테이블을 create하기 때문에 다음과 같은 어노테이션을 Sns, Youtube 엔티티에 달아줘야 합니다.
@OnDelete(action = OnDeleteAction.CASCADE)
public class Youtube {
@Id @GeneratedValue
@Column(name = "youtube_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "candidate_id")
@OnDelete(action = OnDeleteAction.CASCADE) //여기에 추가해 주시면 됩니다.
private Candidate candidate;
...
}
이렇게 변경하고 나면, alter 쿼리에 on delete cascade가 추가되어 날아갑니다.
@Inheritance(strategy = InheritanceType.JOINED)으로 설정된 엔티티의 경우에는
@Entity
@DiscriminatorValue("F")
@OnDelete(action = OnDeleteAction.CASCADE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Facebook extends Sns{
private int likes;
private int comments;
private int shares;
이렇게 해주시면 됩니다.
@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;
@Builder
public Candidate(int number, String name, City city) {
this.number = number;
this.name = name;
this.likes = 0;
this.city = city;
}
}
@Test
public void 후보_삭제_ON_DELETE_CASCADE() throws Exception {
//given
City city = createCity();
Candidate candidate = new Candidate(1, "Jake", city);
candidateRepository.save(candidate);
Facebook facebook = new Facebook(candidate, "content1", "url", LocalDateTime.now(), 1, 1, 1);
facebookRepository.save(facebook);
Youtube youtube = new Youtube(candidate, "url", "title", "thumbnail", LocalDateTime.now(), "description", 1, 1, LocalDateTime.now());
youtubeRepository.save(youtube);
//when
Long candidateId = candidate.getId();
candidateService.delete(candidateId);
//then
assertEquals(0, snsRepository.findAllByCandidateId(candidateId).size());
assertEquals(0, youtubeRepository.findAllByCandidateId(candidateId).size());
}