-
JPA (10) - JPQL 경로 표현식, Fetch join카테고리 없음 2024. 1. 12. 14:20
1. 경로 표현식
> .을 찍어서 객체 그래프 탐색하는 것 의미
//연관관계에 따라 단일 값, 컬렉션값 연관 필드 SELECT m.username // 상태 필드 FROM Member m join m.team t //단일 값 연관 필드 join m.orders o //컬렉션값 연관 필드 where t.name = '팀A'
> 상태 필드 : 단순히 값을 저장하기 위한 필드
> 연관필드 : 연관관계를 위한 필드 (단일,컬렉션)
1.1 경로 표현식 특징
> 상태필드 : 경로탐색 끝
> 단일값 연관 경로 : 묵시적 내부 조인 발
//단일 값 연관 경로 String query = "select m.team from Member m"; //묵시적 내부조인 발생 (하지마라!) String query = "select m.team.name from Member m" //탐색 가능 //컬렉션 값 연관 경로 select t. memebers From Team t //이런식으로 가져옴 //한팀당 멤버 List 가지고 있음 List<List>가 결과일 것 //당연하게도 . 탐색 x (Collection이잖슴, 다만 size정도는 가능) //쓰지말고 명시적 조인을 확용하자 select m from Team t join t.members m; //명시적 조인으로 별칭 얻는 방법
- FROM 절에서 명시적 조인을 통해 별칭 얻으면 별칭을 통해 탐색 가능하다!
1.1 경로탐색 묵시적 조인 주의사항
> 항상 내부 조인임
> 컬렉션은 경로 탐색의 끝이다. 명시적 조인을 통해 별칭 얻자
> 경로 탐색은 주로 SELECT , WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL FROM절에 영향을 준다.
2. Fetch Join
- SQL 조인 종류 X
- JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능이다.
- join fetch 명령어 사용하자
List<Member> members = em.createQuery(query, Member.class).getResultList(); //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
> LAZY 전략이여서 member들만 가져온다. 문제없다.
> 만약 EAGER였으면, 1+N 문제 발생했을 것이다 (즉시 로딩은 em.find()에서만 문제 발생 x -> 식별자 있어서 그러함)
> 이떄 패치조인이 아니라면, 컬렉션 members에서 팀 엔티티를 사용하면 쿼리가 나가기 시작한다 (1+N문제)
String jpql = "select m from Member m join fetch m.team"; List<Member> members = em.createQuery(jpql,Member.class).getResultList(); for(Member member : members) { //패치 조인으로 member에서 Team 메서드 사용해도 문제없다. }
2.1 fetch join의 문제점 (일대다 관계에서 - 일에서 패치조인으로 다를 가져올 경우)
- 컬렉션 패치 조인 문제 (데이터 뻥튀기)
select t from Team t join fetch t.members where t.name = '팀A'
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"; List<Team> teams = em.createQuery(jpql,Team.class).getResultList(); for(Team team : teams){ //회원 정보 꺼낼시 데이터 뻥튀기 됨 }
> 먼저 DB에서 Member가 아닌, Team을 조회한다. 이때 Team.ID = Member.TEAM_ID를 조인조건으로 사용해서 가져오면, 당연히 결과 테이블에는 Team 데이터 중복이 발생한다.
> JPA 입장에선 일단 이 데이터를 모두 가져와야한다. -> List<Team>에 중복 데이터 저장됨
> 추후에 같은 팀 식별자를 가진 Member를 Team에 members에 추가한다. -> 결과적으로 의도하지않은 데이터 뻥튀기 발생
2.2 컬렉션 패치조인 문제 해결법
- SQL의 DISTINCT를 SQL에 추가한다.
- JPQL의 DISTINCT 2가지 제공 기능을 이용한다 (애플리케이션에서 엔티티 중복을 제거)
select distinct t from Team t join fetch t.members where t.name ='팀A'
> SQL 조회 결과에선 실패하지만, JPA가 distinct를보고, 애플리케이션 레벨에서 제거해준다.
> 그냥 distinct 사용하면된다는말!
*하이버네이트 6에서 DISTINCT 명령어 사용하지 않아도 애플리케이션에서 중복 제거 자동 적용
3. 일반 조인과 페치 조인의 차이
3.1 일반 조인
> 일반 조인은 실행시 연관 엔티티 함께 조인하지 않는다. (lazy)
> 조인 조건은 들어가지만, 값을 select는 안함!
> 반환결과에 연관관계 고려하지않는다. 단지 SELECT 절에 지정한 엔티티만 조회한다.
//jqpl 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'
3.2 패치 조인
> 패치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시로딩)
> 패치 조인은 객체 그래프를 SQL 한번에 조회
//jqpl 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'
4. fetch 조인의 한계
> 조인 대상에 별칭 줄 수 있다 (하이버네이트는 가능함! 근데 가급적 사용 금지)
- 하이버네이트는 지원하지만 가급적 사용하지말자 -> 패치조인결과를 다른 패치조인과 연결할때만 사용하셨다함
- 조인 대상을 통해 조작하면 이상한? 결과 발생한다고 함
> 둘 이상의 컬렉션은 페치 조인 할 수 없다. (멤버의 주문 (다) 주문의 멤버 (다))
> 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.
-> 일대일, 다대일 단일 값 연관 필드들은 페치 조인해도 페이징 가능
-> 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험하다!)
- 해결방법 (일대다 컬렉션 조회시 데이터 뻥튀기)
1. BatchSize설정 (일반 조인으로 해결)
String query2 = "select t from Team t"; @BatchSize(size=100) @OneToMany(mappedBy = "team") List<Member> members = new ArrayList<>(); //BatchSize 설정하면, //SELECT * FROM child WHERE child.parent(1,2,3) 100개씩 끊어서 관련 child 가져온다.
2. 다대일 관계로 뒤집어서 사용
String sql = "SELECT m from Member m join fetch m.team"
- 정리하자면, fetch join은 연관된 엔티티들을 SQL 한번으로 조회 가능 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적하과 필요한 곳은 페치 조인 적용
* 페치 조인으로 모든 것을 해결할 수는 없다! 다만, 페치조인은 객체 그래프 유지할 때 사용하면 효과적이다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면, 페치조인말고 일반 조인 사용하고
DTO로 변환하는것이 효과적