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

KANG MIN SOO님의 프로필 이미지
KANG MIN SOO

작성한 질문수

실전! 스프링 데이터 JPA

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

작성

·

655

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

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

안녕하세요. 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
질문자

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

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

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

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

0

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

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

기존에는 엔티팅의 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;
}


}

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

KANG MIN SOO님의 프로필 이미지
KANG MIN SOO

작성한 질문수

질문하기