fetch join이란?
Member 내부 Team 객체가 존재할 때
select m from Member m join jetch m.team;
으로 한번에 조회가 가능.
package com.practice.jpql_sample;
import com.practice.jpql_sample.domain.Member;
import com.practice.jpql_sample.domain.Team;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceContext;
import java.util.List;
@SpringBootTest
@Transactional
public class TestCode {
@PersistenceContext
EntityManager em;
@Test
@Rollback(value = false)
void o(){
Team team1 = new Team();
team1.setName("teamA");
Team team2 = new Team();
team2.setName("teamB");
Member member1 = new Member();
member1.setUsername("kim");
member1.setTeam(team1);
Member member2 = new Member();
member2.setUsername("park");
member2.setTeam(team2);
Member member3 = new Member();
member3.setUsername("Lee");
member3.setTeam(team2);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(team1);
em.persist(team2);
em.flush();
em.clear();
String query = "select m From Member m";
List<Member> findMembers = em.createQuery(query, Member.class)
.getResultList();
for (Member findMember : findMembers) {
System.out.println("findMember = " + findMember.getUsername() + " | " + findMember.getTeam().getName());
}
}
}
다음과 같은 코드가 있다.
member1 / member2 / member3 은 각각 team1이나 team2에 소속되는 테스트 코드고 그걸 조회한다.
이제 쿼리문이 총 몇 개 나가는지 살펴보자
Hibernate:
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
member member0_
Hibernate:
select
team0_.team_id as team_id1_3_0_,
team0_.name as name2_3_0_
from
team team0_
where
team0_.team_id=?
findMember = kim | teamA
Hibernate:
select
team0_.team_id as team_id1_3_0_,
team0_.name as name2_3_0_
from
team team0_
where
team0_.team_id=?
findMember = park | teamB
findMember = Lee | teamB
총 3개가 나간다.
for문 첫번째에서 Member에 대한 전체적인 순회를 해야되므로 DB에 있는 모든 Member 테이블을 영속성 컨텍스트에 넣는다.
그 후 team의 이름을 찾기 위해서 member1과 연관된 팀에 해당하는 테이블을 가져온다. (참고로 여기서 Member와 Team의 연관관계 속 fetch전략은 LAZY다.)
왜? em.flush()와 em.clear()로 영속성 컨텍스트를 완전히 비워버렸기 때문에.
member2도 똑같다. 여기서 member는 영속성 컨텍스트에 담겼지만 member2에 연관된 teamB는 없다.
teamB를 또 불러온다.
“Lee” 는 이미 teamB와 member가 영속성 컨텍스트에 있기때문에 쿼리문이 따로 필요하지 않다.
그럼 한 번에 불러오는 EAGER 전략은 어떤가?
Hibernate:
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
member member0_
Hibernate:
select
team0_.team_id as team_id1_3_0_,
team0_.name as name2_3_0_
from
team team0_
where
team0_.team_id=?
Hibernate:
select
team0_.team_id as team_id1_3_0_,
team0_.name as name2_3_0_
from
team team0_
where
team0_.team_id=?
findMember = kim | teamA
findMember = park | teamB
findMember = Lee | teamB
순서만 바뀌었다 뿐이지 쿼리문 3번 쓰는건 똑같다. EAGER는 순수 member만 조회해도 연관관계에 있는 team을 무조건 전부 가져오기 때문에 LAZY 보다 더 좋지 않다.
자 이제 join fetch를 써보자
String query = "select m From Member m join fetch m.team";
뒤에 join fetch m.team만 붙이면 된다.
Hibernate:
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_3_1_,
member0_.age as age2_0_0_,
member0_.team_id as team_id5_0_0_,
member0_.type as type3_0_0_,
member0_.username as username4_0_0_,
team1_.name as name2_3_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
findMember = kim | teamA
findMember = park | teamB
findMember = Lee | teamB
놀랍게도 쿼리가 3개나 나가던게 join fetch로 한 번에 조회가 끝난다.
join fetch 키워드를 사용하자 hibernate가 inner join을 사용하는 것을 볼 수 있다.
여기서 team은 더이상 프록시가 아니다. inner join으로 모든 데이터를 한 번에 받아왔기 때문에 원본 객체이다.
이 예제는 단순해서 고작 쿼리 3개에서 하나로 줄였을 뿐이지만 만약에 팀이 100개가 있고 멤버의 팀이 모두 다르다면?
조회만 하려고 했는데 쿼리문만 101개가 나갈 것이다.
[(기존 멤버 1) + (팀 100). 보통 N + 1 문제라고 한다.]
그래서 조회 성능의 최적화를 위해서라면 fetch join은 필수적이다.