[JPA] JPQL사용중 LAZY fetch 가 안될 때
발단
복잡한 관계 매핑이나 Native SQL에 가까운 쿼리를 유도해야할때에, JPA Repository 만으로 해결이 힘든 경우가 있다. (보통 가능은 하나, 코드가 지저분해진다)
위와 같은 상황에서 비즈니스 로직을 작성하던 중 있던 일이다. ORM에서 1:M의 OneToMany 관계를 매핑할때는 보통 지연 가져오기를 Fetch 전략으로 선택한다. (Lazy fetching, FetchType.LAZY)
JPA Repository 빈을 가지고 findBy ~~ 하면 이 Lazy fetching이 잘 작동한다. 하지만 JPQL에서는 의도대로 동작하지 않았다.
분석
QueryDSL을 프레임워크를 사용한 JPQL 로직에서 다음과 같은 순서로 이어진다.
1. QClass 엔티티 설정
2. queryDsl 객체로부터 select 절 생성 -> JPQLQuery 객체 리턴
3. JPQLQuery 객체의 fetchResults() 함수 호출 -> QueryResults<Class> 객체 리턴
4. QueryResults<Class> 객체의 getResults() 함수 호출 -> List<Class> 객체 리턴
5. List<Class> 객체를 파라미터로 던져 PageImpl을 통한 Pagination 구현
문제는 4번에서 발생한다. 각 객체들이 가지고 있는 OneToMany 관계의 자식 객체들을 전부다 가져온다. 다시 말해, N+1 쿼리 문제가 발생하게 된다.
이 문제를 해결하기 위해 어노테이션이나 ORM적으로 고민을 많이 했다. 그러다 문득, SQL 관점에서 해결할 수 있을 것 같다는 생각이 들었다.
해결방법
위 문제에 대한 해결방법은 다음과 같다.
1. OneToMany에 대한 자식객체를 투영(프로젝션)하지 않는다.
너무 심플한 방법이었다.
예를들어 특정 기간내에 parentEntity 들을 조회하는 JPQLQuery 객체를 만든다면,
from(parentEntity) .where(parentEntity.regDtime.between(~~~, ~~~))
라고 사용했지만,
다음과 같이 투영할 칼럼들을 명시적으로 지정해주는 것이었다.
getQuerydsl().createQuery(parentEntity) .select(parentEntity.parentColumn1, parentEntity.parentColumn2) .where(parentEntity.regDtime.between(~~~, ~~~))
해결됐다! N+1로 DB 질의를 낭비하던 문제가 사라졌다.
하지만 이 방법의 문제는 return의 타입토큰이 List<Tuple> 이었다. 헉..
이렇게 되면 object mapper로 결과를 또 매핑해주어야 하는 지저분함이 발생하기 때문에 좀 더 머리를 굴렸다.
찾다보니, .select() 로 칼럼들을 지정할때에 어떤 객체를 베이스로 할지 지정할 수 있었다. 사실 createQuery시에 QClass 엔티티객체를 넣기 때문에 알아서 인식할거라고 생각했는데 생각과 달랐다.
따라서 최종적인 해결방법은 다음과 같다.
getQuerydsl().createQuery(parentEntity) .select(Projections.fields(ParentEntity.class, parentEntity.parentColumn1, parentEntity.parentColumn2) .where(parentEntity.regDtime.between(~~~, ~~~))
추가
기존 코드와 개선한 코드에는 차이가 있다.
기존에는 from함수를 사용해 JPQLQuery를 받아왔지만
개선쿼리에는 from절을 직접 사용하지 않고 getQuerydsl().createQuery(path) 를 이용했다.
이 이유는 QuerydslRepositorySupport 클래스를 열어 보면 알 수 있는데,
단일 path(사용할 엔티티라고 생각하면 된다)가 입력될 경우 이 from 함수를 호출하게 된다.
protected <T> JPQLQuery<T> from(EntityPath<T> path) { return getRequiredQuerydsl().createQuery(path).select(path); }
이미 select 함수를 체이닝하여 원하는 결과가 나오지 않을 것 같아, createQuery까지만 QClass 엔티티를 할당해주고 나머지 투영할 칼럼들을 직접 선택하였다.
일반 from절을 사용했을 때 다시 select 를 했을 때 overwritten이 되는지에 대해 beta 배포전까지 알아보려고 한다.
JPQLQuery의 구현체인 HibernateQuery 클래스의 select 함수를 보면 setProjection으로 되어있기 때문에 addProjection이 아니겠다만, 이런 중요한 사항은 직접 시도해보고 결과를 기록하는쪽이 좋다고 생각한다.
4개의 댓글