fetch 조인?
SQL의 조인 종류가 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다 , 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다 join fetch 명령어를 통해 사용한다 .
1. 엔티티 fetch join
회원을 조회하면서 연관된 팀도 함께 조회 하고싶은 경우를 예로들면
1.
// 만약 연관관계에 있는 Team이 모두 다르다면 쿼리가 n+1번으로 나간다.
String query = "select m from Member m ";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member=" +member.getUsername() +" , "+"team="+member.getTeam().getName());
//회원1 , 팀 1(sql)
//회원2 , 팀 2(1차 캐시)
//회원3 , 팀 3(sql)
}
2.
//fetch join의 경우 해당 연관관계가 eagar로 동작한다 .
//진짜 데이터를 가져온다
String query2 = "select m from Member m join fetch m.team";
List<Member> result2 = em.createQuery(query2, Member.class)
.getResultList();
for (Member member : result2) {
System.out.println("member=" +member.getUsername() +" , "+"team="+member.getTeam().getName());
}
1번의 경우 쿼리 3방 , 2번의 경우 1방
연관관계를 지연로딩으로 했어도 , fetch조인이 우선된다.
2.컬렉션 fetch join
//반대편 일다다에서 컬랙션조회를 할떄
//컬렉션 페치조인의 주의점
//일대다 관계에서의 DB 레코드가 뻥튀기 된다 .
// team2 에 2명이 있으니 결과도 2개가 나온다 . team2에 관한 내용도 멤버의 수만큼 나온다.
String query3 = "select t from Team t join fetch t.members";
List<Team> result3 = em.createQuery(query3, Team.class)
.getResultList();
for (Team team : result3) {
System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
}
//위의 문제해결을 위해 JPQL이 distict 명령어를 제공한다 .
//db의 distinct와는 조금 다르다 .
//JPQL의 distinct는 sql에 distinct를 추가해서 날려주고
//리턴 되는 엔티티의 중복을 한번더 체크해준다.
String query4 = "select distinct t from Team t join fetch t.members";
List<Team> result4 = em.createQuery(query4, Team.class)
.getResultList();
for (Team team : result4) {
System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
}
JPQL의 DISTINCT는 같은 식별자를 가진 Team 엔티티를 제거한다 .
일반조인과 페치조인 차이
일반 조인은 조인은하지만 데이터는 가져오지 않는다 . 페치 조인은 조인대상의 데이터까지 모두가져온다 .
System.out.println("==============일반조인=============");
//일반조인
String query5 = "select distinct t from Team t join t.members";
List<Team> result5 = em.createQuery(query5, Team.class)
.getResultList();
for (Team team : result5) {
System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
}
System.out.println("==============페치조인=============");
//페치조인
String query6 = "select distinct t from Team t join fetch t.members";
List<Team> result6 = em.createQuery(query6, Team.class)
.getResultList();
for (Team team : result5) {
System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
}
일반 조인의경우 조인하여 멤버를 가져오지만 멤버의 데이터는 가져오지 않는다 . 지연로딩이 발생하여 루프를 돌며 member의 이름을 출력 할때마다 쿼리가나간다 .
반면 페치조인은 조인을하면서 연관관계 맺은 데이터를 가져온다 . (Eagar처럼 동작한다)
N+1 문제는 페치조인으로 왠만하면 해결이 가능하다.
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다 . (하이버네이트에서는 가능하나 가급적 사용하지 않는게 좋다 )
String query = "select t from Team t join fetch t.members as tm"; //주석을 주면 안된다.
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team team : result) {
System.out.println("teamname=" +team.getName() +" , "+"member="+team.getMembers().size());
}
기본적으로 연결된 것을 모두 끌어오는 것이기 떄문에 페치조인 되는 대상에는 별칭을 주지 않는 것이 관례이다.
t.members 에서 필터링을 해서 원하는 만큼만 가져오고싶다면? 아예 팀이아닌 멤버에서 조회를 해야한다.
- 페이징 API를 사용할 수 없다.
컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다 일대다 관계에서 컬렉션을 조회하면 데이터가 뻥튀기 되는데 페이징을 해버리면 데이터가 맞지 않아 문제가 생길 수 있다 .
이를 이해하기 위해선 왜 일대다 관계에서 데이터가 뻥튀기 되는지를 이해하는 것이 중요하다.
일대다 관계에서 다쪽은 1의 고유키를 외래키로 공유한다.
ex) 팀 2에 멤버가 3명이면 3명은 모두 같은 팀의 아이디를 외래키로 갖고 있다.
만약 팀을 기준으로 멤버를 탐색한다면 ? SQL은 아래와 같을 것이다.
SQL = SELECT * FROM TEAM t JOIN MEMBER m ON m.team_id = t.id;
조인 조건에 해당하는 멤버는 3명이다 멤버 3명모두 팀의 id를 team_id(외래키) 로 갖고 있다.
이떄문에 team(1) 을 기준으로 멤버(N)를 조인하여 데이터를 가져올 때 다시말해 일대다의 관계에서
데이터를 조회하면 레코드 수는 N 개로 뻥튀기된다.
해결방법
//1. 반대로 접근한다 .
//멤버로 조회하여 멤버의 팀을가져온다
//다대일로 연관관계가 바뀌기 때문에 페이징이 가능해진다.
String query1 = "select m from Member m join fetch m.team";
List<Member> result1 = em.createQuery(query1, Member.class)
.setFirstResult(0)
.setMaxResults(4)
.getResultList();
for (Member member : result1) {
System.out.println("team->"+member.getTeam().getName()+", member->"+member.getUsername());
}
첫번쨰 방법은 반대로 접근하는 것이다 멤버를 통해 팀을 가져오고 그 팀에서 멤버를 꺼내는 방식으로 사용한다.
다대일이 되기 때문에 페이징에 문제가 없다
String query3 = "select t from Team t";
List<Team> result3 = em.createQuery(query3, Team.class)
.getResultList();
System.out.println("result3.size() = " + result3.size());
for (Team team : result3) {
System.out.println("team="+team.getName()+",members="+team.getMembers().size());
for (Member member : team.getMembers()){
System.out.println("->member=" + member);
}
}
두번쨰 방법은 팀으로 일단 받아온 후에 엔티티에 @BatchSize를 주는 것이다 .
위의 코드를 보자 JPQL은 team 만 가져오도록 되어 있다 이대로만 실행하면 팀을 조회하는 쿼리가 하나 나가고
각 팀안에 멤버를 찾는 쿼리가 레이지 로딩될 때마다 한번씩 나간다 . N+1 문제가 발생하는 것이다.
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members;
public Team(String name) {
this.name = name;
}
}
team에 batchsize를 줘서 멤버를 찾을때 팀을 in절로 묶어 in절 안에 팀에 해당되는 멤버를 지정한 수 만큼 가져올 수 있다 .
어노테이션이 아닌 글로벌 셋팅으로도 지정할 수 있다 .
모든 것을 페치 조인으로 해결할 수는 없다 , 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다 .
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 , 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
'JPA' 카테고리의 다른 글
JPA 공부 20 - JPQL . 9 벌크 연산 (0) | 2021.06.08 |
---|---|
JPA 공부 19 - JPQL . 8 Named 쿼리 (0) | 2021.06.08 |
JPA 공부 17 - JPQL . 6 경로 표현식 (0) | 2021.06.07 |
JPA 공부 16 - JPQL . 4 JOIN (0) | 2021.06.06 |
JPA 공부 15 - JPQL . 3 페이징 (0) | 2021.06.06 |