@Test void 사용자_조회_페치_조인() { String jpql = "select u from User u join fetch u.team"; List<User> resultList = em.createQuery(jpql, User.class).getResultList(); System.out.println("==========================="); for (User user : resultList) { System.out.println("username : " + user.getUsername()); } System.out.println("==========================="); for (User user : resultList) { System.out.println("username = " + user.getUsername()); Team team = user.getTeam(); System.out.println(team.getClass()); System.out.println("user teamName = " + team.getName()); System.out.println(); } }
fetch join 적용 (fetch type 은 상관 없음)
발생 쿼리 로그
Hibernate: select u.id, t.id u.team_id u.username, t.teamname from user u inner join team t on u.team_id=t.id
출력 로그
=========================== username : 짱구 username : 유리 username : 치타 username : 코난 =========================== username = 짱구 class com.study.jpql.domain.Team user teamName = 해바라기반 username = 유리 class com.study.jpql.domain.Team user teamName = 해바라기반 username = 치타 class com.study.jpql.domain.Team user teamName = 장미반 username = 코난 class com.study.jpql.domain.Team user teamName = 어린이 탐정단
페치 조인을 통해 단 한방의 join 쿼리가 발생하고 결과 엔티티의 연관관계를 jpa 가 마법처럼 매핑 해 주었습니다.
만약 여기서 fetch 키워드를 jpql에서 제거하면 같은 쿼리가 발생합니다.
하지만 jpa는 이를 채워야 할 근거가 없기 때문에 얻어온 t.teamname 과 같은 team의 필드를 그냥 버립니다.
이후에 연관관계의 fetch 옵션 (EAGER/LAZY)에 따라 추가 적인 쿼리를 발생 시킵니다.
컬렉션 페치 조인
팀을 조회 하며 소속된 멤버를 모두 조회하는 @OneToMany의 관계에서의 fetch join도 확인해 보겠습니다.
@Test void 팀_조회_페치_조인() { String jpql = "select t from Team t join fetch t.users"; List<Team> resultList = em.createQuery(jpql, Team.class) .getResultList(); for (Team team : resultList) { System.out.print("team = " + team.getName()); System.out.print(" -> "); for (User user : team.getUsers()) { System.out.print(user.getUsername()+" "); } System.out.println(); } }
Hibernate: select t.id, u.id, t.teamname, u.team_id u.username u.team_id, u.id from team t inner join user u on team0_.id=users1_.team_id
한방 쿼리가 발생하였고 팀 데이터의 컬렉션 연관관계인 users field에 대해 매핑을 잘 해주었습니다.
하지만 데이터를 살펴 보면 뻥튀기가 발생한 것을 알 수 있습니다.
team = 해바라기반 -> 짱구 유리 team = 해바라기반 -> 짱구 유리 team = 장미반 -> 치타 team = 어린이 탐정단 -> 코난
뻥튀기가 발생한 이유는 db 쿼리의 실행결과를 생각해보면 금방 이해할 수 있습니다.
쿼리 실행 결과
t.id | u.id | t.teamname | u.team_id | u.username |
1 | 1 | 해바라기반 | 1 | 짱구 |
1 | 2 | 해바라기반 | 1 | 유리 |
2 | 3 | 장미반 | 2 | 치타 |
3 | 4 | 어린이 탐정단 | 3 | 코난 |
이 데이터를 그대로 result set에 담아서 풀기 때문에 jpa 레벨에서는 team 데이터의 뻥튀기가 발생할 수 밖에 없습니다. @NtoMany관계의 어쩔 수 없는 숙명입니다.
하이버네이트는 이를 위한 jpql 키워드 distinct를 제공합니다.
distinct
키워드는 두가지 역할을 합니다.- 쿼리에 distinct 키워드 (sql의 distinct)
- 어플리케이션 레벨에서 중복 데이터의 제거 (주소 기반)
Hibernate: select distinct t.id, u.id, t.teamname, u.team_id u.username u.team_id, u.id from team t inner join user u on team0_.id=users1_.team_id team = 해바라기반 -> 짱구 유리 team = 장미반 -> 치타 team = 어린이 탐정단 -> 코난