[JPA Error] save할 때 @ManyToOne 필드가 null인 현상
상황
서버를 켜고 질문 등록을 해보는데 500 에러가 발생했다. 혹시나 해서 답변 등록도 해봤는데 똑같이 500 에러가 발생했다.
이 에러는 서버의 문제라서 바로 콘솔창에서 에러를 확인했다.
연관 관계
ERD에 다른 필드는 적지 않고 PK와 FK만 작성했다.
발생한 에러와 원인 고찰
JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation
오류 내용을 보면 Referential integrity constraint violation(참조 무결성 제약 조건 위반)이 발생함을 알 수 있다.
참조 무결성 제약 조건을 위반했다고 나오니 의심 가는 부분은 @ManyToOne을 적용한 FK 뿐이었다. 오류가 난 hibernate 쿼리를 확인해보니 역시나 FK 필드에서 null이 들어왔다.
@Getter
@Setter //@Builder를 적용했었다가 변경이 잦아서 @Setter로 바꿨다.
@Entity
public class Answer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne
private Question question;
@ManyToOne
private SiteUser author;
private LocalDateTime createAt;
private LocalDateTime modifyAt;
}
Question 엔티티는 '질문'으로 한 질문에는 여러 개의 답변이 달릴 수 있다. 그래서 @ManyToOne을 적용했었는데 여기서 무엇이 문제였는지 알 수 없었다.
우선 참조 무결성 위반이라고 하니 외래키를 가져올 때 문제가 있는 것은 아닌지 의심이 들었다.
참조 무결성이란 관련된 테이블의 레코드 간의 관계를 유효하게 하는 규칙으로, 사용자의 실수로 관련 데이터가 삭제되거나 수정되는 것을 막아준다. (https://jwprogramming.tistory.com/53)
나의 경우 해당 에러는 수정이나 삭제가 아닌 저장(등록)할 때 발생했다. JPA는 저장할 때 insert 또는 update 쿼리를 날리기는 하는데 나의 경우 update 쿼리를 날린 것 같다. (이 부분에 대한 자세한 내용은 구글에 "JPA save insert update"를 검색해 보자)
더 자세히는...
질문을 저장할 때 set되는 필드 중 사용자가 있었는데, 이 사용자는 SiteUser 엔티티로 FK 값이었다. 그리고 답변을 저장할 때 set되는 필드에는 사용자(SiteUser)와 질문(Question) 엔티티가 있었고 마찬가지로 FK 값이었다. 아무래도 해당 FK 필드를 set하면서 문제가 있었던 모양이다. 내 의문은 "여기서 무슨 문제가 있었기에 참조 무결성 위반 에러가 발생한 걸까?!"였다.
사실 이 문제를 해결했지만, 정확한 원인은 아직도 모른다... 더 찾아봐야 한다.
해결
문제는 지연 로딩을 적용하여 해결되었다. (해결도 했는데 원인을 모른다니...) 아래는 지연 로딩을 적용한 Question 엔티티이다.
public class Question {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length=200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answers;
@ManyToOne(fetch = FetchType.LAZY) //지연 로딩 적용
private SiteUser author;
private LocalDateTime createAt;
private LocalDateTime modifyAt;
}
Answer 엔티티에도 마찬가지로 FK 필드에 fetch = FetchType.LAZY를 넣어서 다시 실행해보니 잘 동작했다.
아마도 지연 로딩에 대해 더 자세히 알아보면 원인도 알게 되지 않을까?
230116 추가
알고보니 @ManyToOne의 default 값이 FetchType.LAZY 라고 한다. 그렇다면 지연로딩을 적용해서 해결된 게 아니라는 뜻인데 어떻게 해결이 된걸까... 원인은 아직도 알아내지 못했다...
230213 추가
아니 @ManyToOne의 기본값이 LAZY가 아니라 EAGER라는 걸... 알게되었다... ㅋㅋㅋ..ㅋ...ㅋ... 오늘 오전에 올린 포스팅을 통해서 이 문제의 원인을 유추할 수 있을 것 같다. 우선 구글링으로 JPA 참조 무결성 위반이라고 검색하니 삭제할 때 발생한 글이 많았다. 나의 경우는 저장할 때 발생했다.
@ManyToOne(fetch = FetchType.EAGER)의 경우 FK의 엔티티를 가져올 때 PK의 엔티티도 같이 가져온다. 만약, Answer의 엔티티를 조회하면 Question의 엔티티도 함께 조회가 된다. 실제로 select 쿼리문을 확인해보면 Answer 테이블과 함께 Question 테이블도 함께 조회되면서 join으로 처리되는 걸 확인할 수 있다. 즉, Answer의 엔티티를 저장하려면 Question의 엔티티도 조회해야 한다는 뜻이다.
아무래도 답변(Child)이 달려야 할 질문(Parent)이 DB에 없었던 게 아닐까 추측하고 있다. 이 에러는 질문 엔티티가 DB에 없어야 가능한 상황인 것 같다. 하지만 나는 질문 게시판을 클릭해야 답변을 달 수 있도록 해놨었다. 이때 제대로 DB를 확인하지 못했는데 다음에는 이런 문제가 발생하면 DB부터 확인해야할 것 같다.
좀 얼레벌레 끝나는 것 같은데, 동시에 조회하기 위해서는 DB에 이미 Question 엔티티가 저장되어 있어야 가능하다. 하지만 없다면 당연히 null로 확인되고 위와 같은 에러가 발생하게 되는 것이다. 지금 추측한 게 틀릴 수도 있을 것 같아서 앞으로도 관련된 내용을 찾는대로 업데이트 할 계획이다.