경로 표현식
점을 찍어서 객체 그래프를 탐색하는 것을 경로표현식이라고 한다.
상태필드는 단순히 값을 저장하기 위한 필드, 단일값 연관 필드는 엔티티, 컬렉션 값 연관 필드는 컬렉션을 말한다.
List<Member> resultList = em.createQuery("select m from Member m join m.team", Member.class).getResultList();
System.out.println(resultList.get(0).getUsername());
System.out.println(resultList.get(0).getTeam().getName());
단일값 연관 경로는 묵시적으로 내부 join이 발생한다는 특징이 있다. 이에 탐색이 가능하다.
List<String> resultList = em.createQuery("select t.name from Member m join m.team t", String.class).getResultList();
System.out.println(resultList.get(0));
위의 실행 결과는 team의 이름이 잘 출력된다. 단일값 연관 경로 탐색에서 JPQL은 SQL로 번역될때 다음과 같이 번역된다.
묵시적 join이 일어나는 것을 확인할 수 있다. 하지만 이는 굉장히 성능면에서 위험이 있기 때문에 명시적 join을 사용하도록 해야한다.
컬렉션은 묵시적 내부 조인이 발생하고 탐색은 불가능하다. 만약 FROM 절에서 명시적 조인을 통해 별칭을 얻을 경우에는 탐색이 가능하다.
String query = "select m from Team t join t.members m";
String query = "select t.members.username from Team t"; //안됨!!!
t.members가 탐색의 끝이기 때문에 저 내부로 더 들어갈 수 없다. 탐색하고 싶으면 명시적으로 조인을 해서 별칭을 얻어야만 컬렉션 내부를 탐색할 수 있다.
.
결론 : 명시적 join을 쓰는 것이 쿼리 튜닝하기에 좋다!!
이제 정말 중요한.....
Fetch join 👍✨🔅🔅🔅
Member를 조회하면서 Member가 속한 Team도 함께 조회해오고 싶다! 하면 fetch join을 사용하면 된다.
SELECT m from Member m join fetch m.team
이쯤에서 지연로딩과 즉시로딩 그리고 fetch join과 무엇이 다른가 하는 의문이 든다. 뭐가 다른지 예제와 함께 알아보자.
팀A - 회원1
팀B - 회원2, 회원3
이렇게 구성해두고 다음을 실행해본다.
List<Member> results = em.createQuery("select m from Member m", Member.class).getResultList();
System.out.println("===========================");
for (Member res : results) {
System.out.println("team Name : " + res.getTeam().getName());
}
1. 즉시로딩
먼저 즉시 로딩의 경우에는 쿼리가 나가고 멤버를 조회한 후 멤버의 필드 중 Team이 즉시로딩으로 설정되어있는 것을 보고 바로 하나하나 쿼리를 날릴 것이다.
멤버 조회쿼리 (1개)
결과 멤버마다 팀 select 쿼리 (2개)
========
각 멤버의 팀 이름 출력
일단 멤버는 3개가 조회됐는데 멤버마다 팀 쿼리가 나가면 3개가 나가야지 왜 2개가 나가나? 하면 멤버 2와 3이 같은 팀에 소속되어있기 때문에 멤버 3의 팀을 조회하는 시점에서는 1차캐시에 있기때문에 쿼리가 안나가서 그렇다.
2. 지연로딩
멤버 조회쿼리
==========
팀 조회
팀 이름 출력
팀 조회
팀 이름 출력
이런식으로 멤버의 팀에 대해 조회가 이뤄지는 순간에 select 쿼리가 나가는 것을 확인할 수 있다. 지연로딩이나 즉시로딩이나 1+N 문제가 발생하고 있다.
3. fetch join
쿼리가 한번만 나가서 모든 값을 다 가져온다... 이처럼 패치조인을 사용할 경우 1+N 문제를 해결하여 성능을 매우 향상시킬 수 있다.
컬렉션 Fetch join
위에서는 다대일 상황에서 fetch join이었고 이번엔 일대다 상황에서 fetch join이 어떻게 동작하는지 보자.
List<Team> results = em.createQuery("select t from Team t join fetch t.members", Team.class).getResultList();
System.out.println("===========================");
for (Team res : results) {
System.out.println("team Name : " + res.getName());
}
분명 팀은 2갠데 결과는 3개다.... 이유는 일대다 상황에서는 디비 쿼리 결과가 더 많아질 수 밖에 없기 때문이다.
이를 방지하기 위해서는 JPQL에서 distinct 키워드를 붙여주면 된다. 데이터베이스 입장에서는 모든 결과가 다르니 distinct가 제대로 동작하지 않지만 JPA가 동일한 엔티티일 경우에 중복처리를 해준다.
List<Team> results = em.createQuery("select distinct t from Team t join fetch t.members", Team.class).getResultList();
System.out.println("===========================");
for (Team res : results) {
System.out.println("team Name : " + res.getName());
}
결과가 중복없이 잘 나오는 것을 확인할 수 있다.
! fetch join과 일반 join의 차이
일반 join은 실행시에 연관 엔티티들을 함께 조회하지는 않는다.
List<Team> results = em.createQuery("select t from Team t join t.members m", Team.class).getResultList();
System.out.println("===========================");
for (Team res : results) {
System.out.println("team Name : " + res.getName());
System.out.println("res.getMembers().indexOf(0) = " + res.getMembers().get(0).getUsername());
}
Team을 조회할때에 member은 조회하지 않기때문에 지연로딩에 의해 이후에 member에 접근할때에 긴 조회쿼리가 따로 나가는 것을 확인할 수 있다.
마무리
다음엔 fetch join의 한계에 대해 공부해보겠다
관련글
'백엔드 > JPA' 카테고리의 다른 글
스프링 데이터 JPA 기초 (0) | 2021.08.22 |
---|---|
JPQL 페치조인의 한계, 다양한 쿼리 (0) | 2021.08.09 |
JPQL 기초 (0) | 2021.08.08 |
JPA 값타입 (0) | 2021.08.08 |
JPA 프록시와 영속성 전이 (0) | 2021.08.06 |