JPQL에는 fetch 조인이라는게 있는데 이게 실무에서 정말정말 중요하다고 한다.
패치 조인(fetch)은
SQL 조인의 종류가 아닌데, JPQL에서 성능 최적화를 위해서 제공하는 기능이다.
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능을 제공한다.
"join fetch" 와 같이 명령어를 사용한다.
패치 조인은 LEFT JOIN FETCH [OUTER 생략 가능] / INNER JOIN FETCH 와 같이 조인 경로를 쓰면 된다.
엔티티 패치 조인은
회원을 조회하면서 연관된 팀도 함께 조회한다. (SQL을 날리는 트랜잭션 한 번에)
SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT 된다.
[JPQL]
select m from Member m join fetch m.team
[SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
예시)
※ 엔티티 패치조인 케이스 (다대일)
패치 조인을 적용하지 않고 모든 멤버들을 조회할 경우는 아래와 같다.
jpql = "select m from Member m";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
for (Member m : result) {
System.out.println("m = " + m.getUsername() + ", " + m.getTeam().getName());
}
Hibernate:
/* select
m
from
Member m */ select
member0_.id as id1_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_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
m = 회원1, teamA
m = 회원2, teamA
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
m = 회원3, teamB
쿼리는 총 3번 나갔다.
① Member 조회 ② Team 조회 ③ Member 조회
Member 별 데이터 조회를 살펴보면
(1) 회원1, 팀A(SQL)
(2) 회원2, 팀A(영속성 1차 캐시)
(3) 회원3, 팀B(SQL)
팀이 다 달랐다면 이 이상의 조회 쿼리가 나갔을 수 있다.
회원 100명을 조회한다 할 때, → N + 1 (첫번째 쿼리) ==> N+1 문제가 발생한다.
이는 성능을 떨어뜨릴 가능성이 높다.
Fetch Join 을 사용하면 조회 쿼리가 한번에 나간다.
// 패치 조인을 쓰면 N+1 문제가 해결됌
jpql = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
for (Member m : result) {
System.out.println("m = " + m.getUsername() + ", " + m.getTeam().getName());
}
Hibernate:
/* select
m
from
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.id as 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_.id
m = 회원1, teamA
m = 회원2, teamA
m = 회원3, teamB
Fetch Join을 사용하면 조회 성능이 올라간다.
※ 컬렉션 패치 조인 케이스 (일대다)
→ 데이터가 뻥튀기 될 가능성이 있음(중복건 조회 가능성 높음)
[JPQL]
select t
from Team t join fetch t.members
where t.name = ‘팀A'
[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class).getResultList();
for (Team t : result) {
System.out.print("t = " + t.getName() + ", " + t.getMembers().size());
for (Member m : t.getMembers()) {
System.out.print(" - " + m.getUsername());
}
System.out.println();
}
Hibernate:
/* select
t
from
Team t
join
fetch t.members */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
t = teamA, 2 - 회원1 - 회원2
t = teamA, 2 - 회원1 - 회원2
t = teamB, 1 - 회원3
teamA의 회원들이 중복되어 출력되었다.
이 현상은 distinct를 추가하면 된다
jpql = "select distinct t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class).getResultList();
for (Team t : result) {
System.out.print("t = " + t.getName() + ", " + t.getMembers().size());
for (Member m : t.getMembers()) {
System.out.print(" - " + m.getUsername());
}
System.out.println();
}
Hibernate:
/* select
distinct t
from
Team t
join
fetch t.members */ select
distinct team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
t = teamA, 2 - 회원1 - 회원2
t = teamB, 1 - 회원3
SQL에서의 DISTINCT는 중복된 결과를 제거하는 역할을 하는데
JPQL 에서의 DISTINCT는 실행될 쿼리 SQL에 DISTINCT를 추가하고, 애플리케이션에서 엔티티 중복을 제거한다.
DISTINCT를 추가한 결과로 같은 식별자를 가진 Team 엔티티를 제거한다.
[DISTINCT 추가시 결과]
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
패치 조인과 일반 조인의 차이가 존재한다.
일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'
[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
단지 SELECT 절에 지정한 엔티티만 조회한다.
위의 쿼리는 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.
패치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다(즉시 로딩)
패치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.
패치 조인은 연관 엔티티를 함께 조회한다.
예시)
[JPQL]
select t
from Team t join fetch t.members
where t.name = ‘팀A'
[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
'자바 > JPA' 카테고리의 다른 글
[JPA/Java] JPQL 다형성 쿼리 - TYPE, TREAT (0) | 2023.05.11 |
---|---|
[JPA/Java] JPQL 패치 조인의 한계 - 글로벌 배치 패치 사이즈, @BatchSize (0) | 2023.05.11 |
[JPA/Java] JPQL 경로 표현식 (0) | 2023.05.10 |
[JPA/Java] JPQL 기본함수 및 사용자 정의 함수 호출 (0) | 2023.05.10 |
[JPA/Java] JPQL CASE 문, COALESCE, NULLIF (0) | 2023.05.09 |
댓글