인프런 커뮤니티 질문&답변

KANG MIN SOO님의 프로필 이미지

작성한 질문수

실전! 스프링 데이터 JPA

복합키 식별관계 재질문입니다.

21.09.08 19:28 작성

·

642

0

안녕하세요 김영한님!

자유주재로 올렸다가 요청해주신것에 따라서 질문으로 옮겼습니다.

 

현재 spring data jpa로 진행중이며 간략한 엔티티 및 관계 정의는 아래와 같습니다.

Entity A [primary key A1, A2] / LectureType.class

IdClassA [A1, A2]

Entity B [primary key A1, A2, B1] / ManyToOne 단방향 관계, fetch lazy / ExamType.class

IdClassB [IdClassA, B1]

 

ex) BeforeAll로 A,B 더미 데이터를 저장

    @Transactional 

    @Test

         Brepository.findAll();

         Brepository.findAll(); 

 

 

B Repository로 findall을 두번호출했을때 ( 다른 코드는 없습니다 )

identifier of an instance of B was altered from BIdClass@90c990a9 BIdClass@21b621d7 와 같이 예외가 발생했습니다. (키 값을 변경하려고 시도한적도 없습니다)

 

이와 관련해서 제가 개념을 잘못 익힌것인지, 검색 컨셉을 잘못잡은것인지 모르겠지만, 검색해도 잘 안나오더라구요.

각 엔티티의 주키를 string으로 직접 저장하는것 때문인지, 정확한 문제를 모르겠습니다.

 

공유 링크 : https://drive.google.com/file/d/1fnbHq8i1gcZC0eWFuCYolDnRzwOqio2x/view?usp=sharing

실행 방법 : spring data jpa 강의를 그대로 따라 한것이여서 강의에서 설명해주신것과 실행 방식이 다르지 않습니다. 다만 h2 데이터베이스를 별도로 다운받지 않고,

내장된 것을 사용중입니다. application.yml에 정보가 추가 되어 있습니다.

문제 발생되는곳 : test/java/study/datajpa/domain/exam/ExamTypeRepositoryTest 에서 exam_type_crud_check() 테스트 실행하면 에러 내용을 확인 할 수 있습니다.

 

하나더 궁금한 것이 있는데 id를 직접 넣어서 할때 강의해주신 Persistable<String>을 사용하면 된다고 하셨는데, 복합키 혹은 식별 관계에 있는 복합키일 경우 IdClass를 String 부분에 넣어주면 되는것일까요??

 

 

답변 2

1

김영한님의 프로필 이미지
김영한
지식공유자

2021. 09. 09. 22:28

안녕하세요. KANG MIN SOO님

저도 테스트해보았는데, 하이버네이트 JPQL 쿼리 파서 버그로 보입니다.

우선 어떤 상황에 발생하냐면 정확하게는 다음이 아니라

repository.findAll();

repository.findAll();

 

다음 상황에 발생합니다.

repository.findAll();

repository.flush(); 

그러니까 플러시 시점에 문제가 되는 것이지요.

 

중간에 하이버네이트가 ID 값을 바꾸어버립니다.

그 이유는 바로 다음 때문인데요.

 

실패

select

    exam.exam_type_category as exam_typ1_0_,

    exam.lecture_type_category_id as lecture_0_0_,

    exam.lecture_type_category_id as lecture_4_0_,

    exam.lecture_type_level_id as lecture_5_0_,

    exam.created_date as created_2_0_,

    exam.exam_type_name as exam_typ3_0_ 

from

    exam_type exam

하이버네이트는 쿼리 시점에 좀 불필요하지만, 연관된 컬럼도 한번 더 조회하는 특징이 있습니다. 그런데 잘 보면 lecture_type_category_idlecture_type_level_id를 두번 조회해야 하는데, lecture_type_level_id 조회가 한번 빠진 것이 보입니다.

컬럼 이름을 바꾸어서 실행하면 성공하는데 다음을 보시면

-------------

성공

select

    exam.exam_type_category as exam_typ1_0_,

    exam.a_lecture_type_category_id as a_lectur0_0_,

    exam.b_lecture_type_level_id as b_lectur0_0_,

    exam.a_lecture_type_category_id as a_lectur4_0_,

    exam.b_lecture_type_level_id as b_lectur5_0_,

    exam.created_date as created_2_0_,

    exam.exam_type_name as exam_typ3_0_ 

from

    exam_type exam

 

제가 임의로 컬럼이름을 바꾸었는데 보시면 잘 조회하는 것을 확인할 수 있습니다.

a_lecture_type_category_idb_lecture_type_level_id

a_lecture_type_category_idb_lecture_type_level_id

 

이렇게 JPQL 조회 시점에 문제가 발생하는데 이 때문에, 하이버네이트 내부에서 식별자를 다시 생성하고 바꾸는 이상한 과정이 진행됩니다.

이 문제는 사실 일반적으로 발생하는 것은 아니고 제가 확인해보았을 때 다음에서 발생합니다.

1. @IdClass + 2개 이상의 컬럼을 조합한 PK를 연관관계로 사용할 때만 발생

2-1. db 컬럼명의 앞 8자리가 같으면 오류(다음의 경우 오류)

PK1: aaaaaaaax

PK2: aaaaaaaaz

2-2. 숫자 앞의 문자가 같으면 오류(숫자를 기준으로 토큰을 짜르는 것 같습니다. 다음의 경우 오류)

aaa1

aaa2

 

그래서 다음의 경우 db 컬럼명의 앞 8자리가 같기 때문에 오류가 발생했고, 

lecture_type_category

lecture_type_level

다음의 경우 오류가 발생하지 않았습니다.

lt_category

lt_level

정리하면 JPQL 파서와 관련된 하이버네이트 버그로 보입니다. 불편하시겠지만 JPQL 관련 이슈는 해결이 쉽지 않아서 이 부분을 회피하는 방법을 선택하셔야 할 것 같습니다. (아마도 하이버네이트 6 버전이 나와야 해결될 것 같아요)

JPA에서 가장 좋은 식별자 전략은 가급적 복합키를 사용하지 않고, 비즈니스에 의존하지 않는 시퀀스, Identity같은 임의의 단일 값을 사용하는 것입니다.

도움이 되셨길 바래요^^

KANG MIN SOO님의 프로필 이미지
KANG MIN SOO
질문자

2021. 09. 09. 23:46

김영한님이 말씀하셨듯이 일반적이지 않아서

같은 조건인데 어떤 경우는 되고 어떤 경우는 안되서

제가 학습하는 부분에 대해 이해를 잘못하고 있는가 했습니다.

이해하기 쉽게 답변 주셔서 너무 감사드립니다!

0

KANG MIN SOO님의 프로필 이미지
KANG MIN SOO
질문자

2021. 09. 09. 20:04

이것저것 해보다가 특정 부분을 수정했더니 테스트가 정상적으로 진행되는 부분이 있어서 답변하시는데 좀 더 수월하시지않을까 하고 추가 정보를 올립니다.

기존에는 엔티팅의 id 컬럼 명칭을 아래와 같이 했습니다. 변경전

public class LectureType {

@Column(name = "lecture_type_category")
@Id
private String category;

@Column(name = "lecture_type_level")
@Id
private String level;

@Column(name = "lecture_type_name")
private String name;

@Column(name = "lecture_type_description")
private String description;

@Builder
public LectureType(String category, String level, String name, String description) {
this.category = category;
this.level = level;
this.name = name;
this.description = description;
}


}
public class ExamType {


@Column(name = "exam_type_category")
@Id
private String category;


@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "lecture_type_category", referencedColumnName = "lecture_type_category"),
@JoinColumn(name = "lecture_type_level", referencedColumnName = "lecture_type_level")
})
@Id
private LectureType lectureType;

@Column(name = "exam_type_name")
private String name;

@Builder
public ExamType(String category, LectureType lectureType, String name) {
this.category = category;
this.lectureType = lectureType;
this.name = name;
}
}

컬럼명칭을 변경했습니다. 변경후

public class LectureType {

@Column(name = "lt_category")
@Id
private String category;

@Column(name = "lt_level")
@Id
private String level;

@Column(name = "lecture_type_name")
private String name;

@Column(name = "lecture_type_description")
private String description;

@Builder
public LectureType(String category, String level, String name, String description) {
this.category = category;
this.level = level;
this.name = name;
this.description = description;
}
}
public class ExamType {


@Column(name = "exam_type_category")
@Id
private String category;


@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "lt_category", referencedColumnName = "lt_category"),
@JoinColumn(name = "lt_level", referencedColumnName = "lt_level")
})
@Id
private LectureType lectureType;

@Column(name = "exam_type_name")
private String name;

@Builder
public ExamType(String category, LectureType lectureType, String name) {
this.category = category;
this.lectureType = lectureType;
this.name = name;
}


}

변경후에는 테스트가 정상적으로 통과했습니다.