JPA > Fetch Join , BatchSize
JPQL 에서 성능 최적화를 위해 제공하는 기능으로, 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회할 수 있다.
(일반 조인 시 연관엔티티를 함께 조회하지 않고, 사용할 때 새로 select 해온다.)
select m from Member m join fetch m.team
=>
SELECT M.* , T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
즉시 로딩과 유사하게 모든 데이터를 한 번에 Inner Join 으로 가져온다.
Fetch Join 에서는 어떤 객체 그래프를 한번에 조회할 것인지 명시한 것이 다르다.
2021.04.25 - [BackEnd/자바&스프링] - JPA > 즉시로딩 , 지연로딩 에서 언급한 N +1 문제를 해결할 수 있는 방법 중
하나가 fetch join 문이다. 조인문으로 한번에 모든 데이터를 가져온다.
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_ID4_0_0_,
member0_.userName as userName3_0_0_,
team1_.name as name2_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
결과를 얻기 위한 코드에서도 Member 객체 하나로 Team 에 대한 데이터를 이용할 수 있다.
for (Member m :resultList){
System.out.println("m.getUserName() = " + m.getTeam().getName());
}
명시적 조인과 fetch join 의 차이
명시적 조회
SELECT m,t from Member m join m.team t
Hibernate:
select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.age as age2_0_0_,
member0_.TEAM_ID as TEAM_ID4_0_0_,
member0_.userName as userName3_0_0_,
team1_.name as name2_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
실행된 쿼리는 동일해 보인다.
하지만 결과를 얻기 위해 작성해야 하는 코드는 좀 더 복잡하다.
Object o = resultList.get(0);
Object[] result = (Object[]) o;
Member m = (Member) result[0];
Team t = (Team) result[1];
System.out.println("result[0] = " + m.getUserName());
System.out.println("result[0] = " + m.getTeam().getName());
System.out.println("result[1] = " + t.getName());
명시적 조인은 결과에 Member,Team 타입 두 개가 있어 결과를 얻기위해 더 번거로운 작업이 필요한데 비해,
fetch join을 사용하면 Member 객체 하나로 깔끔한 객체 그래프를 받을 수 있다.
fetch join의 핵심은 성능 최적화와 객체 그래프 이다.
일대다 관계에서 데이터 중복
컬렉션을 이용해 fetch join 을 하면 데이터가 중복 생성된다.
Member (다) 과 Team (일) 의 관계에서, Team 의 members 컬렉션을 조회하는 경우이다.
teamA 에 회원1, 회원2 있으면
teamA 은 하나인데, 회원쪽 데이터가 2개이니 팀을 조회한 row 결과가 2줄 나오게 된다.
아래와 같이 데이터 갯수가 잘못 나오는 것이다.
select t from Team t join fetch t.members; //teamA 에 회원2명 teamB에 회원1명
select t from Team t; 의 데이터 수는 3개 !
select t from Team t join fetch t.members; 의 데이터 수는 4개 !
distinct 로 중복을 제거할 수 있는데, sql 레벨에서 distinct 로는 데이터가 제대로 제거되지 않는다.
그래서 JPQL 의 distinct 는 Member 엔티티의 식별자를 이용해 한 번 더 중복을 제거해준다.
다대일에서는 이런 중복이 일어나지 않는다.
주의 사항
fetch join 대상에는 별칭을 가급적 사용하지 않도록 해야한다.
아래와 같이 작성해서 팀과 연관된 회원 5명 중 1명만 불러온다고 가정한다.
select t from Team t join fetch t.members m where m.username=....;
이러면 객체 그래프에서는 team 에 있는 나머지 4명의 회원에 대한 데이터가 누락이 되고 select 해온 t 에서는
1명에 대한 데이터 밖에 없기 때문에 잘못된 사용과 잘못된 결과를 낳을 수 있다.
객체 그래프라는 것은 기본적으로 데이터를 모두 조회하는 것이 좋다.
만약 이렇게 몇개의 데이터만 골라서 가져오고 싶은 경우에는 team 에서 member 를 조회하는 아래 fetch join 방식이 아니라 처음부터 필요한 member 를 조회하도록 짜고, team 의 member 를 모두 조회하는 객체 그래프는 따로 설계한다.
fetch join 을 여러개 연결해서 사용하는 경우에는 별칭을 이용하는 경우가 생길 수 있지만, 그런 상황은 거의 발생하지 않고, JPA 의 설계 목적에 맞게 사용하도록 노력해야한다.
둘 이상의 컬렉션은 페치 조인 하면 좋지 않다. 일대다도 데이터가 잘못 나올 수 있는데 이런 다대다 조합은 위험하다.
컬레션을 페치 조인하면 페이징 API 를 사용할 수 없다.
일대일, 다대일 같은 단일 값 연관 필드들은 페치조인을 써도 페이징 API 에 문제가 없다.
하이버네이트는 경로 로그를 남기고 메모리에서 페이징을 하는데 무척 위험하다.
일대다 fetch join 에 페이징 == 사용하면 안됨!
List<Team> result =
em.createQuery("select t from Team t join fetch t.members m ",Team.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
1. 이런 케이스는 쿼리를 뒤집어 사용할 수 있다.
List<Member> result =
em.createQuery("select m from Member m join fetch m.team t",Member.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
2. @BatchSize 사용
- fetch join 을 제거
List<Team> result =
em.createQuery("select t from Team t",Team.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
이렇게만 하면 Lazy Loading 으로 team 에서 member 를 꺼내 쓸 때 member 를 조회하는 쿼리가 계속 나간다.(N+1)
public class Team {
@BatchSize(size=100)
@OneToMany(mappedBy="team")
public List<Member> members = new ArrayList<>();
...
}
지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.
size는 IN절에 올수있는 최대 인자 개수를 말한다.
team 에서 member 를 가져올 때 100개씩 가져온다.
select
memberlist0_.TEAM_ID as TEAM_ID4_0_1_,
memberlist0_.id as id1_0_1_,
memberlist0_.id as id1_0_0_,
memberlist0_.age as age2_0_0_,
memberlist0_.TEAM_ID as TEAM_ID4_0_0_,
memberlist0_.userName as userName3_0_0_
from
Member memberlist0_
where
memberlist0_.TEAM_ID in (
?, ?
)
혹은 @BatchSize 어노테이션 대신 아래와 같이 persistence.xml 에 설정을 추가해도 된다.
<property name="hibernate.default_batch_fetch_size" value="100" />
1. fetch join 으로 엔티티 조회해서 사용
2. 애플리케이션에서 dto 로 변환해서 쓴다
3. 처음부터 new dto 로 스위칭해서 가져온다
www.inflearn.com/course/ORM-JPA-Basic/lecture/21742?tab=community&speed=1.25&q=170331
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 학습 페이지
지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....
www.inflearn.com