본 포스팅은 인프러의 JPA 기본편을 수강하고 정리하는 내용입니다.
페치 조인의 특징과 한계
1) 페치 조인 대상에는 별칭(alias)을 줄 수 없다.
- 하이버네이트는 가능, 가급적 사용X
- 예시
1
2
3
String query = "select distinct t from Team t join fetch t.members as m where m.age > 10";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
- 왜 안되냐? 기본적으로 나와 연관된 것들을 다 긁어오는 것인데 5명중 3명만 불려와서 조작하면 이상하게 동작할 수 있다. 잘못하면 이상한 데이터가 삭제 될 수 있다.
- 차라리 별도의 쿼리로 Member 5개만 불러오는게 맞다.
- 별칭이 기본적으로 허용된다면 데이터의 정합성이나 객체 그래프의 사상에 맞지 않게 된다.
- 하지만 join fetch a join fetch b … 처럼 할때가 있는데 그때만 딱 쓴다.
2) 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 데이터 정합성에 안맞는다.
- 잘못하면 데이터가 예상치 못하게 팍팍 늘어나면서 곱하기 곱하기가 될 수 있다.
딱 하나만 지정할 수 있다.
3) 컬렉션(일대다)을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- 만약 여기서 페이징이 적용되면 회원1만 남게된다. 그러면 팀A는 회원1만 가지고 있는걸로 되버린다. 문제가 있다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
- 실제 쿼리에 페이징 구문이 없다? -> 팀이 100만건이면 100만건을 다 메모리에 올리고 페이징한다. 바로 그냥 망한다.
- 세 가지 해결법이 있다.
- 1)회원에서 팀으로 방향을 반대로 뒤집어 해결하는 방법이 있다.
1 2 3 4 5
String query = "select m from Member m join fetch m.team t"; List<Member> result = em.createQuery(query, Member.class) .setFirstResult(0) .setMaxResults(1) .getResultList();
- 2)
@BatchSize(size = 100)
을 지정하여 해결할 수 가 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
@Entity public class Team { @Id @GeneratedValue private Long id; private String name; @BatchSize(size = 100) @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); ... } public class JpaMain { public static void main(String[] args) { ... String query = "select t from Team t"; List<Team> result = em.createQuery(query, Team.class) .setFirstResult(0) .setMaxResults(2) .getResultList(); System.out.println("result.size() = " + result.size()); for (Team team : result) { System.out.println("team = " + team.getName() + " | " + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("- members = " + member); } } tx.commit(); } } SQL: Hibernate: /* load one-to-many jpql.Team.members */ select members0_.TEAM_ID as team_id5_0_1_, members0_.id as id1_0_1_, members0_.id as id1_0_0_, members0_.age as age2_0_0_, members0_.TEAM_ID as team_id5_0_0_, members0_.type as type3_0_0_, members0_.username as username4_0_0_ from Member members0_ where members0_.TEAM_ID in ( ?, ? )
- 처음에 Team테이블 select 쿼리 1번 + List결과에 담긴 Team객체를 100개씩 in절에 담아 members객체에 채워넣는다.
- global 세팅에 가져갈 수 있다.
1
<property name="hibernate.default_batch_fetch_size" value="100" />
- 3)DTO로 쿼리 직접 짜는 방법(DTO로 뽑아도 정제해줘야하기에 만만치 않다)
4) 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
5) 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
- @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
7) 실무에서 글로벌 로딩 전략은 모두 지연 로딩
8) 최적화가 필요한 곳은 페치 조인 적용
페치 조인 - 정리
- 모든 것을 페치 조인으로 해결할 수 는 없음
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서
DTO
로 반환하는 것이 효과적
복잡한 조인이 많은 쿼리의 경우(통계성 쿼리 같은 경우) 사실 세 가지 방법이 있다.
1) 페치 조인으로 엔티티를 조회해 와서 그대로 쓴다
2) 페치 조인 열심히해서 애플리케이션에서 DTO로 바꿔쓴다.
3) 아예 처음부터 조인해서 DTO로 결과를 받아와 쓴다.