-
#JPA-21 JPQL - 페치 조인(fetch join)JPA 2021. 2. 23. 17:39
[출처] 인프런 김영한 강사님 -자바 ORM 표준 JPA 프로그래밍 기본
실무에서 정말정말 중요함
페치 조인(fetch join)
• SQL 조인 종류X
• JPQL에서 성능 최적화를 위해 제공하는 기능
• 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능 (쿼리 두번나갈꺼 한방쿼리로 풀때!)
• join fetch 명령어 사용 • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
(쿼리를 내가 원하는대로 어던 객체그래프를 한번에 조회할거야하는것을 내가직ㅈ버 명시적으로 동적인 타이밍에 정할수 있는것!)
• 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
• SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
• [JPQL]
select m from Member m join fetch m.team
SELECT M이라고 하나만 했는데 M,T DATA 둘다 나열한다. 이게 중요!!! (즉시 로딩이랑 비슷)
• [SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
Table , query result, 1차 캐시 Jpa가 entity 5개를 만들어서 1차캐시에 이렇게 보관을 하고, fetch join 그림을 만들어서 한방쿼리로 반환한다.
JpaMain
Team teamA = new Team(); teamA.setName("팀A"); em.persist(teamA); Team teamB = new Team(); teamB.setName("팀B"); em.persist(teamB); Member member1 = new Member(); member1.setUsername("회원1"); member1.setTeam(teamA); em.persist(member1); Member member2 = new Member(); member2.setUsername("회원2"); member2.setTeam(teamB); em.persist(member2); Member member3 = new Member(); member3.setUsername("회원3"); member3.setTeam(teamB); em.persist(member3); em.flush(); em.clear(); 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()+ ", "+member.getTeam().getName()); //여기서 Member 에서의 team은 lazy 지연로딩 때문에 proxy로 설정되어있고 //, memger.getTeam()을 호출할때마다 DB에서 값을 가져온다. } tx.commit();
여기서 Member 에서의 team은 lazy 지연로딩 때문에 proxy로 설정되어있고, memger.getTeam()을 호출할때마다 DB에서 값을 가져온다.
console
Hibernate: /* select m From Member m */ select member0_.id as id1_0_, member0_.age as age2_0_, member0_.TEAM_ID as TEAM_ID5_0_, member0_.type as type3_0_, member0_.username as username4_0_ from Member member0_
여기서 member 3개를 들고옴.
Hibernate: select team0_.id as id1_3_0_, team0_.name as name2_3_0_ from Team team0_ where team0_.id=? member = 회원1, 팀A
Member들은 이미 영속성 컨텍스트에 있고
iterator로 첫 번째 member를 가지고 올때 getTeam.getName()을 할때 team이 프록시인걸 알고 영속성컨텍스트에 team을 요청하여 DB에서 찾아서 영속성 컨텍스트에 반영한다.
Hibernate: select team0_.id as id1_3_0_, team0_.name as name2_3_0_ from Team team0_ where team0_.id=? member = 회원2, 팀B member = 회원3, 팀B
두번째 member을 가지고올때 getTeam.getName()을 하면서 team객체를 DB에서 찾아서 영속성 컨텍스트에 넣고세번째 member를 가지고올떄 getTeam.getName()을 하면 1차 캐시에 이미 두번째 team이랑 같기때문에 DB에 갓다오지 않고 바로 결과가 나온다.
현재 경우에는 쿼리가 3번나갓다 게시판이라고 생각하고 다른 회원 100명이면 쿼리가 100번나간다.. 이런경우를 N +1 (첫번째날린쿼리로 날린 RESULT결과 만큼 N번 도는것)
이문제를 fetch join으로 해결한다.
페치 조인 사용 코드
console
Hibernate: /* 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_ID5_0_0_, member0_.type as type3_0_0_, member0_.username as username4_0_0_, team1_.name as name2_3_1_ from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.id member = 회원1, 팀A member = 회원2, 팀B member = 회원3, 팀B
- Member를 조인하긴 할건데 일반적 sql join을 하긴하는데 fetch라는 것으로 한번에 끌고와 끌고오는데 뭘끌고 오냐 team을 끌고와.
- 여기서 member.getTeam의 team은 proxy가 아니다.이미 데이터를 다 조인해서 다들고온상태이다. proxy가아니라 실제 enitty가 담긴것이다.
- 지연로딩을 세팅해도 패치조인이 세팅이 항상 먼저이다.
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
컬렉션 페치 조인
• 일대다 관계, 컬렉션 페치 조인
• [JPQL]
select t from Team t join fetch t.members where t.name = ‘팀A'
팀입장에서 meber를 조인하는 것 위에 것이랑 반대
console
Hibernate: /* select t From Team t join fetch t.members */ select team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as TEAM_ID5_0_1_, members1_.type as type3_0_1_, members1_.username as username4_0_1_, members1_.TEAM_ID as TEAM_ID5_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID team = 팀A| members = 1 team = 팀B| members = 2 team = 팀B| members = 2
왜 team = 팀A| members = 1, team = 팀B| members = 2 가 중복되는지 ?
우리가 원하는 데로 데이터는 가지고오는데 문제가 뭐냐면?
join이기때문에 데이터가 뻥튀기 될수도 있다. 이건 아래 이미지에서 설명하겠다.
• [SQL]
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
DB의 처리결과는 회원이 두명이기때문에 기본적으로 db result는 2번째 표처럼 만든다 그래서 두줄로 만든다.
teamA입장에선 하난데 Member는 두 줄이된다.
DB에서 두줄가지고오면 그냥 두줄 가져와야한다. DB에서 주면 우선 받아야한다.
여기서 ID(PK)가 1번이면 영속성컨텍스트에 올려넣고 두번째도 1번이면 같은 영속성 컨텍스트를 쓰는데
조회한 컬렉션에는 같은 주소값을 가지는게 두명이나온다.
컬렉션 페치 조인 사용 코드
console
Hibernate: /* select t From Team t join fetch t.members */ select team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as TEAM_ID5_0_1_, members1_.type as type3_0_1_, members1_.username as username4_0_1_, members1_.TEAM_ID as TEAM_ID5_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID team = 팀A| members = 1 member = jpql.Member@51ec2856 team = 팀B| members = 2 member = jpql.Member@33634f04 member = jpql.Member@4993febc team = 팀B| members = 2 member = jpql.Member@33634f04 member = jpql.Member@4993febc
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
(1:N 은 뻥튀기 , N:1은 뻥튀기 안됨 개수가 맞거나 없으면 줄어듬)
페치 조인과 DISTINCT
• SQL의 DISTINCT는 중복된 결과를 제거하는 명령
• JPQL의 DISTINCT 2가지 기능 제공
• 1. SQL에 DISTINCT를 추가
• 2. 애플리케이션에서 엔티티 중복 제거 (똑같은 entity가 있으면 제거)
페치 조인과 DISTINCT 가 안되는 경우
• select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’
• SQL입장에서의 DISTINCT
SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과 에서 중복제거 실패 (완전히 똑같아야 distinct가 된다)
console
Hibernate: /* select distinct t From Team t join fetch t.members */ select distinct team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as TEAM_ID5_0_1_, members1_.type as type3_0_1_, members1_.username as username4_0_1_, members1_.TEAM_ID as TEAM_ID5_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID
쿼리에선 distinct가 되지만쿼리만으로는 distinct가 되지 않는다.
페치 조인과 DISTINCT
• DISTINCT가 추가로 애플리케이션에서 중복 제거시도
• 같은 식별자를 가진 Team 엔티티 제거
[DISTINCT 추가시 결과]
console
Hibernate: /* select distinct t From Team t join fetch t.members */ select distinct team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as TEAM_ID5_0_1_, members1_.type as type3_0_1_, members1_.username as username4_0_1_, members1_.TEAM_ID as TEAM_ID5_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID team = 팀A| members = 1 member = jpql.Member@1caa9eb6 team = 팀B| members = 2 member = jpql.Member@7601bc96 member = jpql.Member@48a0c8aa
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
페치 조인과 일반 조인의 차이
• 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
• [JPQL]
select t from Team t join t.members m where t.name = ‘팀A'
console
Hibernate: /* select t From Team t join t.members m */ select team0_.id as id1_3_, team0_.name as name2_3_ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID Hibernate: select members0_.TEAM_ID as TEAM_ID5_0_0_, members0_.id as id1_0_0_, members0_.id as id1_0_1_, members0_.age as age2_0_1_, members0_.TEAM_ID as TEAM_ID5_0_1_, members0_.type as type3_0_1_, members0_.username as username4_0_1_ from Member members0_ where members0_.TEAM_ID=? team = 팀A| members = 1 member = jpql.Member@53c6f96d Hibernate: select members0_.TEAM_ID as TEAM_ID5_0_0_, members0_.id as id1_0_0_, members0_.id as id1_0_1_, members0_.age as age2_0_1_, members0_.TEAM_ID as TEAM_ID5_0_1_, members0_.type as type3_0_1_, members0_.username as username4_0_1_ from Member members0_ where members0_.TEAM_ID=? team = 팀B| members = 2 member = jpql.Member@3c98781a member = jpql.Member@3f736a16 team = 팀B| members = 2 member = jpql.Member@3c98781a member = jpql.Member@3f736a16
컬렉션은 프록시는 아닌데 데이터가 로딩시점에 없기때문에 쿼리가 쭉쭉 나감
-> 조인 패치로 배꾸게 되면 쿼리한번에 나가고 select절에 데이터 다포함되있고 지연로딩이 없다.
• [SQL]
SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
페치 조인과 일반 조인의 차이
• JPQL은 결과를 반환할 때 연관관계 고려X
• 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
• 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
• 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
• 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
페치 조인 실행 예시
• 페치 조인은 연관된 엔티티를 함께 조회함
• [JPQL]
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'
페치 조인의 특징과 한계
• 페치 조인 대상에는 별칭을 줄 수 없다.
• 하이버네이트는 가능, 가급적 사용X
- 패치조인은 나랑연관된 것을 다끌고 오겠다는 건데, 성능상 중간에 몇개를 걸러오고싶다고 해도 안된다.
- 걸러오고싶으면 따로 조회해야한다.
- 팀과 연관된 애들이 5명이라고 치자 그중에 한명만 불러올려고하는데 잘못조작하게되면 나머지 4명이 누락되있어 서 이상하게 동작할 수 있다.
- 이렇게 때문에 별칭 주지 않는게 관례이다.
- 팀을조회하는데 맴버가 5명이있는데 그중에 3명만 조회했어 3명만 따로 조작한다는거 자체가 위험하다.
왜냐하면 jpa에서 객체그래프를 . 으로 찍어서 가면 팀에서 멤버로간다 원래 5명이 나와야하는데 where m.age>10 로 3개만 뽑혓다.
team.mebers로가면 3개명 밖에 안나오는거다 app입장에선 jpa에서 의도한 설계는 이렇게 부부능로 조회하는 것이 아니고 team.members할때 맴버 한태 다갈수 있어야한다.
객체 그래프라는 것은 기본적으로 데이터를 다 조회해야하는거다.
거르면서 조회하는게 좋을거같지만 이건 team에서 members를 가져오면 안되고 처음부터 맴버를 5개 조회해야한다. 연관관계를 찾아간다는 것은 team.members를 하면 members를 다 찾아간다고 가정하고 설계되어잇다.
이상한옵션에 다발라져 있으면 나머지가 이상하게 나올 수 도있다. 결론 alias는 웬만해서 쓰면안된다.
- 사용하는 경우 -join fetch를 몇단계로 가져갈떄
• 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 1:N:N 라서 데이터가 예상하지 못하게 늘어나면서 ** 가 될수 있다. 데이터가 잘안맞다.
- 패치조인 컬렉션은 딱하나만 슬 수 있다.
• 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
• 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 일대다는 데이터 뻥튀기가 된다 이걸로 페이징을 한다? 왜안되냐면?
- TEAMA가 DATA2건이라치고 이걸 패치조인하면 2건나온다. PAGE사이즈 1로 줫다.
그럼 반으로 잘리고 하나만 가져온다 그럼 TEAMA는 회원 1만 가지고 있다라고 생각하기 때문에 안된다.
- PAGE는 DB중심적이고 ROW수를 어떻게 줄일지를 생각하는건데 그런데 컬렉션을 패치조인하면
JPA에서는 TeamA 하나고 얘는 회원 1밖에 없어 라고 결과가 나온다.
• 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험) 절대 쓰면안된다.
JpaMain Class
String query = "select t From Team t join fetch t.members as m"; List<Team> result = em.createQuery(query, Team.class) .setFirstResult(0) .setMaxResults(1) .getResultList();
console
Hibernate: /* select t From Team t join fetch t.members as m */ select team0_.id as id1_3_0_, members1_.id as id1_0_1_, team0_.name as name2_3_0_, members1_.age as age2_0_1_, members1_.TEAM_ID as TEAM_ID5_0_1_, members1_.type as type3_0_1_, members1_.username as username4_0_1_, members1_.TEAM_ID as TEAM_ID5_0_0__, members1_.id as id1_0_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID
- firstResult/maxResults specified with collection fetch; applying in memory! 라는 에러가 나온다.
그럼 컬렌셕을 패치조인 할수있는 방법은 없을까?
-1. 지금 캐이스에서는 일대다 다대일이면 반대로 할 수 있기때문에
쿼리를 바꾸는것이다.
회원에서 팀으로가는것은 다대일이기 떄문에 문제가 없다.
-2. 패치조인을 사용하지 않는것.
JpaMain
console.
Hibernate: /* select t From Team t */ select team0_.id as id1_3_, team0_.name as name2_3_ from Team team0_ limit ? Hibernate: select members0_.TEAM_ID as TEAM_ID5_0_0_, members0_.id as id1_0_0_, members0_.id as id1_0_1_, members0_.age as age2_0_1_, members0_.TEAM_ID as TEAM_ID5_0_1_, members0_.type as type3_0_1_, members0_.username as username4_0_1_ from Member members0_ where members0_.TEAM_ID=? team = 팀A| members = 1 member = jpql.Member@53d13cd4 Hibernate: select members0_.TEAM_ID as TEAM_ID5_0_0_, members0_.id as id1_0_0_, members0_.id as id1_0_1_, members0_.age as age2_0_1_, members0_.TEAM_ID as TEAM_ID5_0_1_, members0_.type as type3_0_1_, members0_.username as username4_0_1_ from Member members0_ where members0_.TEAM_ID=? team = 팀B| members = 2 member = jpql.Member@429f7919 member = jpql.Member@4a2929a4
되긴되지만 쿼리가 3번이나 돌았다 팀이 10개면 추가로 더늘어남 성능상 좋지않다.
-3. batchsize
teamId 가?,? 두개가 들어가있다 . 한번에 teamA에 연관된 맴버와 teamB연관된 맴버를 다들고 오는것이다.
이옵션이 무슨옵션인가 하면은 team을 가져올때 현자 member는 Lazy loading 상태이다 그런데 lazy loading을 딱
끌고올대 내팀뿐만아니라 list에 담긴 팀을 한번에 in query로 100개씩 넘긴다 지금은 2개바께없으니깐 2개만 넘긴다.
이걸이용하면 table이 team을 패이징 쿼리하는데 10개가있는데 team과 연관된 걸 찾는 쿼리가 10번이 나가야하는데
그래서 팀조회 1번 맴버 조회 10번 N+1 문제 (N 팀을가져온거에 결과 갯수) 를 해결할 수 있다.(Fetch 조인으로도 해결할 수있다.
• 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
• 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
• @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
• 실무에서 글로벌 로딩 전략은 모두 지연 로딩
• 최적화가 필요한 곳은 페치 조인 적용
페치 조인 - 정리
• 모든 것을 페치 조인으로 해결할 수 는 없음
• 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
• 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면,
페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
-3가지 방법
1. entity를 패치조인을써서 entity를 조회해온다.
2. 패치조인을 열심히해서 application 에서 dto로 바꿔서 화면에 ㅂ=ㅏㄴ화
3. 처음부터 new operation으로 스위칭해서 가져온다.
'JPA' 카테고리의 다른 글
#JPA-22 JPQL- 다형성 쿼리, 엔티티 직접 사용, Named 쿼리 (0) 2021.02.25 #JPA-20 JPQL- 경로 표현식 (0) 2021.02.23 #JPA-19 페이징 API, JOIN, JPA 기타 (0) 2021.02.23 #JPA-18 JPQL(Java Persistence Query Language) (0) 2021.02.23 #JPA-17 JPA가 지원하는 다양한 쿼리 방법 (0) 2021.02.23