두 테이블의 서로 연관관계가 없는 테이블의 데이터를 조회할 때 Querydsl의 fetchJoin이 왜 동작하지 않는지 궁금했다.
Querydsl의 FetchJoin() 기능은 데이터베이스에 존재하는 기능이 아니라,
JPA를 사용하는 환경에서 연관된 엔티티를 한 번의 쿼리로 가져오기 위해 사용되는 JPA의 기능이라고 알고 있지만 호출하여 사용이 가능하다.
실제 동작은 fetchJoin 기능이 작동하진 않기 때문에 쿼리는 일반적인 JOIN 으로 실행되는데, 내부적으로 어떤 동작에 의해서 실행이 되지 않는지 찾아보았다.
회원이 즐겨찾기 시 BookDetail테이블의 PK인 ID가 Bookmark테이블의 BOOK_ID 칼럼에 들어간다. 1:N 관계이지만, 연관관계가 맺어지지 않은 상태이다.
| BOOK_DETAIL | BOOK_MARK |
|---|---|
| ID | ID |
| CATEGORY_ID | BOOK_ID |
| BOOK_NAME | USER_ID |
| AUTHOR_ID | DESCRIPTION |
시나리오는 특정 카테고리의 책들을 즐겨찾기한 회원 목록을 보여줘야 한다.
public List<BookmarkDto> findCategoryBookmark(String categoryId) {
return queryFactory
.select(Projections.fields(BookmarkDto.class
, bookDetailEntity.categoryId
, bookmarkEntity.bookId
, bookmarkEntity.userId
, bookmarkEntity.description
))
.from(bookDetailEntity)
.innerJoin(bookmarkEntity).on(bookDetailEntity.id.eq(bookmarkEntity.bookId))
.fetchJoin()
.where(bookDetailEntity.categoryId.eq(categoryId))
.fetch();
}
BookDetail과 Bookmark을 맵핑할 수 있는 칼럼인 book_id 를 통해 이너 조인을 했다. 이때 연관관계가 없는 두 테이블을 fetchJoin() 할 경우 어떻게 동작하는 지 확인할 것이다.
.innerJoin(bookmarkEntity).on()
innerJoin() 를 보면 JPQL을 생성하는 queryMixin.innerJoin을 호출한다.
참고로 Querydsl에서 SQL이 되는 과정 중 JPQL 로 변환한다.queryMixin.innerJoin() 를 보면 조인 타입으로 INNERJOIN이 있고, 이때 매개변수 target은 조인하는 테이블인 bookmarkEntity이다.
metadata.addJoin 의 코드에서 안쪽 if 조건문을 보면
if (expr instanceof Path && ((Path)expr).getMetadata().isRoot())exprInJoins 목록에 추가해서 Join 쿼리문을 만든다.elsevalidate(expr) 메서드를 호출하여 조인의 유효성을 검증한다.BookDetail과 Bookmark은 연관 관계가 아니기 때문에 if (expr instanceof Path && ((Path)expr).getMetadata().isRoot())조건문에 부합하고 이때 inner join 절이 만들어진다.with 키워드로 변경된다..fetchJoin()
fetchJoin() 를 보면 JPQL을 생성하는 queryMixin.fetchJoin을 호출하고 있다.
queryMixin.innerJoin()를 보면 이때 addJoinFlag() 메소드를 통해 FETCH 키워드가 붙게 된다.
JPQL
select
bookDetailEntity.categoryId,
bookmarkEntity.bookId,
bookmarkEntity.userId,
bookmarkEntity.description
from BookDetailEntity bookDetailEntity
inner join fetch BookmarkEntity bookmarkEntity
with bookDetailEntity.id = bookmarkEntity.bookId
이너 조인과 fetch 키워드가 있고 on → with 키워드로 변경되었다. with키워드로 변경된 이유는 문법적 차이로 Hibernate는 with 키워드를 사용하여 조인 조건을 지정하기 때문이다.
네이티브 SQL
select
bookDetailEntity.categoryId,
bookmarkEntity.bookId,
bookmarkEntity.userId,
bookmarkEntity.description
from BookDetailEntity bookDetailEntity
inner join BookmarkEntity bookmarkEntity
on (bookDetailEntity.id = bookmarkEntity.bookId)
하지만 실행된 네이티브 SQL문은 fetch키워드가 없어져 있다.(별칭 생략)
fetch 키워드가 사라진 이유는 뭘까?
먼저 fetch 키워드는 JPQL에서 연관된 엔티티나 컬렉션을 한 번에 가져오기 위한 명령어로 사용되지만, SQL 표준에는 JPQL의 fetch 키워드와 같은 기능이 없다.
먼저 JPA 구현체는 JPQL의 fetch키워드를 사용하여 생성된 쿼리를 분석하고, 필요한 조인을 추가하여 관련 데이터를 한 번에 가져올 수 있도록 네이티브 SQL을 생성한다.(위 디버깅 과정) 이때 fetch 키워드가 제거되고 조인 조건만 포함한 SQL을 생성한다.
결과적으로 JPQL의 fetch 키워드는 JPA 레벨에서의 데이터 로딩 전략을 지정하는 것이고, 네이티브 SQL로 변환될 때는 SQL 표준 문법에 맞게 조인 구문으로 변환된다. 이 과정에서 fetch 키워드는 사라지지만, JPA는 여전히 연관된 엔티티를 즉시 로딩하게 되는 것이다.
querydsl 의 공식문서를 보면 연관관계가 되어 있는 테이블 간에 Join을 사용하고 on() 메소드를 사용하고 있지 않다.


즉, 연관관계인 Cat과 Kitten을 조인하되 Kitten의 bodyWeight가 10.0보다 작은 경우는 제외된다는 것을 의미하는 조건절이다.
따라서 innerJoin().on()을 사용하여 연관관계가 없는 테이블 간에 조인을 수행하는 경우 단순히 on() 절의 조건에 맞는 네이티브 SQL을 생성하는 것이다.
그렇기 때문에 JPA에 의해 처리되는 연관된 엔티티의 Fetch 전략을 사용할 수 없다.
JPA의 구현체인 Hibernate의 공식문서를 보면
"fetch" 조인은 부모 객체와 연관된 속성이나 컬렉션의 값들을 단일 select 쿼리로 한 번에 초기화하는 것을 허용합니다. 이는 컬렉션의 경우에 특히 유용합니다. 이는 매핑 파일에서의 outer join과 lazy 선언을 효과적으로 무시하고 연관 및 컬렉션에 대한 조인을 수행합니다. 이를 통해 데이터베이스에서 부모 객체와 그에 속한 연관된 객체 또는 컬렉션을 한 번의 쿼리로 모두 가져올 수 있습니다.
fetch Join에 대해 데이터베이스에서 연관된 객체 또는 연관된 컬렉션을 즉시 로딩한다고 말한다. 따라서 연관관계가 없으면 fetch Join 을 사용할 수 없는 것이다.
정의대로 fetchJoin은 일반적으로 연관관계가 명확히 정의된 엔티티에서 사용되어야 한다. 연관관계가 맺어지지 않은 엔티티에 fetchJoin()사용 시 혼란이 생길 수도 있기 때문에 fetchJoin() 을 제거하는 것이 적절하다.
참고: http://querydsl.com/static/querydsl/5.0.0/reference/html/ch02.html#d0e317
https://docs.jboss.org/hibernate/orm/3.3/reference/ko-KR/html/queryhql.html#queryhql-joins