[ElectionPJT/JPA] JPA의 외래 키 무결성 / Candidate Entity에서 양방향 매핑을 포기한 이유
1. 양방향 매핑을 하려고 했던 이유
- Candidate를 삭제할 경우, Cascade(영속성 전이)를 통해 쉽게 관련 sns, youtube까지 삭제하고 싶었습니다.
- Total likes, Total comments 등 통계 기능을 도입할 때 편할 것 같았습니다.
2. 양방향 매핑을 포기하는 이유
처음 생각: 삭제하기 위한 Candidate를 id로 조회할 때, 관련 엔티티(sns, youtube)들을 함께 fetch 조인해야 cascade가 잘 적용되겠지?
그래서 join fetch 쿼리를 작성하던 중 김영한님의 인프런 강의에서 들었던 내용이 생각났습니다.
- 둘 이상의 컬렉션은 페치 조인하면 안된다
- @ToMany 페치 조인에서는 페이징을 사용할 수 없다
- 둘 이상의 컬렉션을 페치 조인할 경우, 1:N:N 매핑이 되어 자료가 말도 안되게 뻥튀기 될 위험이 있으며, 정합성 이슈가 발생할 수도 있다.
- 위와 같은 이유로, @ToMany 페치 조인에서는 페이징 사용이 제한되며, 만약 억지로 사용하면 메모리에서 페이징이 되기 때문에 굉장히 위험하다.
또한 Total likes, Total comments 등 통계 기능은 Sns, Youtube 테이블을 모두 조인해서 결과를 내야하는데... 강의 내용에 따르면
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
- 따라서 양방향 매핑을 더 이상 사용할 이유가 없어졌습니다.
3. 그렇다면 참조 무결성은 어떻게 보장해야 할까
현재 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;
이렇게 해주시면 됩니다.
4. 변경된 코드
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;
@Builder
public Candidate(int number, String name, City city) {
this.number = number;
this.name = name;
this.likes = 0;
this.city = city;
}
}
- 연관관계 메서드 삭제
Candidate Service
5. Candidate 엔티티 변경 후 테스트
테스트 코드
@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());
}
테스트 결과
Author And Source
이 문제에 관하여([ElectionPJT/JPA] JPA의 외래 키 무결성 / Candidate Entity에서 양방향 매핑을 포기한 이유), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@mincho920/ElectionPJTJPA-JPA의-외래-키-무결성-Candidate-Entity에서-양방향-매핑을-포기한-이유저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)