JPA 오류 : ConstraintViolationException

YEON·2022년 4월 4일
1

JPA

목록 보기
4/5

🙋‍♀️ 아직 정리가 진행중인 포스팅입니다.

영속성 컨텍스트에 대한 이해가 부족하여 생긴 기초적인 오류에 관하여 정리해보고자 합니다.

❗ 오류

Repository에 단순히 객체를 save 하려 했을 때, 다음과 같은 오류가 발생

could not execute statement; SQL [n/a]; constraint [FK_ANSWER_TO_WRITER]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [FK_ANSWER_TO_WRITER]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement

현재 객체는 다음과 같이 존재한다.

Answer

public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name="fk_answer_to_writer"))
    private User writer;
}

User

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    ...
    @OneToMany(mappedBy = "writer", cascade = CascadeType.ALL)
    private List<Answer> answer = new ArrayList<>();
}


❕ 원인

연관관계를 맺어주었지만, 연관관계를 맺어주는 엔티티 또한 영속 상태여야한다.
영속 상태로 관리되어야지만 관계를 맺을 수 있다.
= 연관관계를 맺어주려면 연관관계를 맺어주는 엔티티 클래스들은 영속 상태여야한다.

Answer을 집어넣어주려했는데 그 안의 User는 영속 상태가 아니므로 user_id 가 없고
그래서 id 가 없는 것을 테이블에 insert 해주려다보니까 실패하게 되는 것이다. (null 값이던 아니던)


따라서,

 public static final Answer A1 = new Answer(User.KIM, "Answers Contents1");

다음을 AnswerRepository에 Answer A1을 save 하고자 한다면,
User.KIM 도 함께 save 가 된 상태여야 이 객체들을 가져다가 save 가 가능하다.

@Test
void save() {
	userRepository.save(User.KIM);
    final Answer actual = answerRepository.save(AnswerTest.A1);
    assertThat(actual.getId()).isEqualTo(AnswerTest.A1.getId());
}

추가로, 만약 여기서 양방향 연관관계로 설정을 해줬음에도
양방향 연관관계를 저장해주지 않는다면 다음과 같은 오류가 발생할 수 있다.
(연관관계가 실패하여 save 가 제대로 이루어지지 않아 IDENTITY 전략에 따라 id 값이 생성될 수 없었기 때문에 발생한 것이다)




❕ 주의할 점

다음과 같은 코드에서 @GeneratedValue 속성인 (strategy = GenerationType.IDENTITY) 를 생략하면 따로 User을 저장하지 않아도 Answer 객체에 관하여 save 가 무사히 잘 작동되었다.

public class Answer {
    @Id @GeneratedValue
    private Long id;
    ...
}
 public static final Answer A1 = new Answer(1L, User.KIM, "Answers Contents1");

그 이유는 무엇일까?

GenerationType.IDENTITY 전략
: 기본키 생성을 데이터베이스가 자동으로 생성해주는 것이다.
그러므로 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있고, JPA가 기본 키 값을 가져오려면 데이터베이스를 추가로 조회하는 방식이다.
만약 처음부터 id(1L)을 저장해주게 되면 save 에 실패하게 된다. (이 부분에서 오류가 발생했다)

save 관점에서 다시 생각해보면
영속성 컨텍스트에 저장되려면 id(PK)가 필요하고 GenerationType.IDENTITY 전략을 사용할 경우 현재 id 는 존재하지 않는다. 때문에 전략상 id 값을 넣고 가져오기 위해 데이터베이스에 우선 insert해서 id를 가져와 이때부터 insert 쿼리가 영속화된다.
(save는 부모 트랜잭션이 없으면 바로 @Transactional에 의해서 트랜잭션이 되기 때문에 가능하다.)


이건 save 가 아닌 다른 메서드에서도 발생될 수 있는 오류이다. ex) findByName
영속성 컨텍스트의 경우 findById() 같은 경우는 영속성 컨텍스트를 먼저 찾고 해당 엔티티가 있으면 그 값을 바로 리턴한다. (1차 캐시 개념)
하지만 JPQL 의 경우 영속성 컨텍스트를 먼저 조회하지 않고 데이터베이스에 query하여 결과를 가져온다.
그래서 가져온 값이 1차 캐시에 존재하는 경우 데이터베이스에서 조회한 신규 데이터를 버린다. (없으면 저장한다)





[참조]
https://velog.io/@dkajffkem/DataIntegrityViolationException-%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

https://cheese10yun.github.io/jpa-persistent-context/

https://web-km.tistory.com/46

profile
- 👩🏻‍💻

0개의 댓글