사이드프로젝트 진행중 까다로운 데이터 구조를 다루게 되었다.
데이터 구조는 계층형 구조로 A -> B -> C 의 구조로 종속되어있다.
A에서 B의 외래키를 가지고있고 B에서 C의 외래키를 가지고 있는 구조이고, jpa repository에서 조회하게 되면 N + 1 문제도 생기게 된다.
그러면 근본적으로 테이블 구조를 바꿔볼 수 없을까라고 생각하여 상속관계 매핑을 고려해보았는데 부모 클래스에 있는 데이터를 아래 자식 클래스에서 사용할일이 없다. 즉 확장의 개념이 아니기 때문에 이 방법은 사용하지 않기로 했다.
그렇다면 기존 데이터 구조는 유지하고 보다 효율적인 쿼리를 작성해보자라는 결론이 나왔다.
JPQL의 경우 쿼리를 String으로 직접 작성해줘야하기때문에 짜증난다.
그래서 컴파일 시점에서 에러를 잡을 수 없고... 개인적으로 가독성이 굉장히 안좋다고 생각한다.
그리하여 QueryDsl을 사용해서 해결해보기로 했다.
// JpaRepository에서 호출
repository.findByEmailAndSubjectId(email, id);
Hibernate:
/* select
generatedAlias0
from
UserCheckList as generatedAlias0
where
(
generatedAlias0.email=:param0
)
and (
generatedAlias0.subjectId=:param1
) */ select
usercheckl0_.id as id1_6_,
usercheckl0_.email as email2_6_,
usercheckl0_.subject_id as subject_3_6_
from
user_check_list usercheckl0_
where
usercheckl0_.email=?
and usercheckl0_.subject_id=?
Hibernate:
select
usercheckl0_.user_check_list_id as user_che3_8_0_,
usercheckl0_.id as id1_8_0_,
usercheckl0_.id as id1_8_1_,
usercheckl0_.section_title as section_2_8_1_,
usercheckl0_.user_check_list_id as user_che3_8_1_
from
user_check_list_section usercheckl0_
where
usercheckl0_.user_check_list_id=?
Hibernate:
select
elements0_.user_check_list_section_id as user_che4_7_0_,
elements0_.id as id1_7_0_,
elements0_.id as id1_7_1_,
elements0_.element_title as element_2_7_1_,
elements0_.is_checked as is_check3_7_1_,
elements0_.user_check_list_section_id as user_che4_7_1_
from
user_check_list_element elements0_
where
elements0_.user_check_list_section_id=?
연관관계의 fetchType이 Lazy로 되어있기 때문에 필요한 시점에 n번의 쿼리가 더 날아가는것을 확인할 수 있었다.
아직 프로덕션에 올라가지않은 서비스이지만 충분히 문제가 될만한 부분이었다.
이 부분을 QueryDsl로 우아하게 처리해보자.
fun findByEmailAndSubjectId(email: String, subjectId: Long): UserCheckList? {
val userChecklist = QUserCheckList.userCheckList
val section = QUserCheckListSection.userCheckListSection
val userCheckLists = query.selectFrom(userChecklist)
.leftJoin(userChecklist.userCheckListSections, section)
.fetchJoin()
.leftJoin(section.elements)
.fetchJoin()
.where(
QUserCheckList.userCheckList.email.eq(email)
.and(QUserCheckList.userCheckList.subjectId.eq(subjectId))
)
.fetch()
// join을 하게되면 테이블이 합쳐지게 되는데 이 과정에서 중복 데이터가 발생한다.
// 따라서 직접 중복 제거를 한다.
.stream()
.distinct()
.collect(Collectors.toList())
return if(userCheckLists.isEmpty()) null else userCheckLists[0]
}
Hibernate:
*/ select
usercheckl0_.id as id1_6_0_,
usercheckl1_.id as id1_8_1_,
elements2_.id as id1_7_2_,
usercheckl0_.email as email2_6_0_,
usercheckl0_.subject_id as subject_3_6_0_,
usercheckl1_.section_title as section_2_8_1_,
usercheckl1_.user_check_list_id as user_che3_8_1_,
usercheckl1_.user_check_list_id as user_che3_8_0__,
usercheckl1_.id as id1_8_0__,
elements2_.element_title as element_2_7_2_,
elements2_.is_checked as is_check3_7_2_,
elements2_.user_check_list_section_id as user_che4_7_2_,
elements2_.user_check_list_section_id as user_che4_7_1__,
elements2_.id as id1_7_1__
from
user_check_list usercheckl0_
left outer join
user_check_list_section usercheckl1_
on usercheckl0_.id=usercheckl1_.user_check_list_id
left outer join
user_check_list_element elements2_
on usercheckl1_.id=elements2_.user_check_list_section_id
where
usercheckl0_.email=?
and usercheckl0_.subject_id=?
쿼리가 한번만 나가는것을 볼 수 있다.
추가적으로 이 데이터는 한번에 다 가져와야하는 데이터라 컬럼을 따로 지정해서 가져오진 않았다.
JPQL대신 QueryDsl로 처리를 해보았는데 기존에 JPQL의 경우
"select * from data " +
"where id = blah"
위와같이 작성해주어야 해서 굉장히 까다로웠다.
하지만 QueryDsl을 도입함으로써 편리함과 가독성을 가져갈 수 있었다.
QueryDsl을 다루는데에는 아직 미숙하지만 공부하면서 더 잘 쓰게 된다면 굉장히 좋은 라이브러리일것같다.
잘못된 부분은 지적 해주시면 감사하겠습니다.