Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- spring
- jpa
- JPQL
- 백준
- Servlet
- 그리디
- SpringBoot
- springdatajpa
- Thymeleaf
- Spring Boot
- 김영한
- transaction
- 자바
- AOP
- Greedy
- 스프링 핵심 원리
- 스프링
- http
- QueryDSL
- JDBC
- 스프링 핵심 기능
- Proxy
- java
- 알고리즘
- kotlin
- db
- pointcut
- Android
- 인프런
- Exception
Archives
- Today
- Total
개발자되기 프로젝트
FETCH JOIN 본문
실무에서 저어어어어어어어ㅓ엉어어어엉말 중요함.
1. fetch join
- SQL 조인의 종류가 아님
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능임.
- join fetch 명령어 사용
- fetch join ::=[LEFT (OUTER) | INNTER] JOIN FETCH 조인경로
- 쿼리로 내가 원하는 객체 그래프를 어느 시점에 한 번에 조회할 것이라고 정할 수 있는 것.
- 지연로딩으로 설정해도 fetch join의 우선순위가 높음.
2. Entity fetch join
- 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
- SQL을 보면 회원 뿐 만 아니라 팀(T.*)도 함께 SELECT
- JPQL
select m from Member m join fetch m.team
- SQL: JPQL에서는 select 프로젝션에 m만 있는데, 실행된 SQL에는 M.*과 T.* 둘 다 있다.
-
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
-
3. 예제
- member1 : 팀A
- member2 : 팀A
- member3 : 팀B
- 지연로딩으로 설정되어 있기 때문에, 처음 Team은 프록시로 불러온다.
- team.getName 시점에 프록시 초기화됨.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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(teamA);
em.persist(member2);
Member member3 = new Member();
member2.setUsername("회원3");
member2.setTeam(teamB);
em.persist(member2);
em.flush();
em.clear();
String query = "select m From Member m";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();
for (Member member : resultList) {
System.out.println("member = " + member.getUsername() + "," + member.getTeam().getName());
//회원1, 팀A(SQL)
//회원2, 팀A(1차캐시)
//회원3, 팀B(SQL)
}
tx.commit();
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_
Hibernate:
select
team0_.TEAM_ID as team_id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.TEAM_ID=?
member = 회원1,팀A
member = 회원2,팀A
Hibernate:
select
team0_.TEAM_ID as team_id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.TEAM_ID=?
member = 회원3,팀B
- N+1 이슈 : 나는 member는 가지고오는 쿼리를 날렸는데, 연관된 Team을 가져오는 쿼리도 날라감.
- 즉 1번 째 쿼리에 해당하는 N개의 엔티티의 연관 엔티티까지 모두 SQL로 호출됨.
- N+1은 즉시로딩이든, 지연로딩이든 발생함.
- 해법은 fetch join임
4. fetch join 적용
- N+1 이슈 해결책.
- join을 할건데, 한번에 team까지 가져와
String query = "select m From Member m join fetch m.team";
- 이 때 join으로 가져온 Team은 프록시가 아님. 진짜 데이터 가져옴. 데이터가 다 채워져있음.
Hibernate:
/* select
m
From
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.TEAM_ID as team_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_.TEAM_ID
member = 회원1,팀A
member = 회원2,팀A
member = 회원3,팀B
5. 컬렉션 fetch join
- 일대다 관계, 컬렉션 fetch join
- 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'
-
String query = "select t From Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class).getResultList();
for (Team team : resultList) {
System.out.println("team.getName() = " + team.getName()+" team.getMembers().size() : " + team.getMembers().size());
}
- 어? Team A가 두 번 나왔네?
- DB입장에서 일대다 조인하면 데이터가 뻥튀기됨. ㄷㄷ
team.getName() = 팀A team.getMembers().size() : 2
team.getName() = 팀A team.getMembers().size() : 2
team.getName() = 팀B team.getMembers().size() : 1
- teams 결과리스트 컬렉션을 보면 팀A가 두개 존재한다.
- 첫 번 째 팀A는 영속성 컨텍스트에 등록이 되고 두 번째 팀A를 영속성 컨텍스트에서 가져다가씀.
- Team만 불러오면 결과는 팀A, 팀B 두 개 이지만
- join fetch t.members의 경우는 두 테이블이 조인되기 때문에
팀A-member1, 팀A-member2, 팀B-member3으로 사이즈가 커진다! 중요한 차이! - 즉 데이터베이스에서 일대다 조인할 때 데이터가 뻥튀기 될 수 있다.(중복이 있다.)
6. fetch join과 DISTINCT
- 중복이 싫은데..?
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령
- JPQL의 DISTINCT는 2 가지 기능을 제공
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복제거
-
select distinct t from Team t join fetch t.members where t.name = '팀A'
- SQL에 DISTINCT 추가해도 데이터가 다르므로 SQL결과에서 중복제거 실패함.
- 즉 쿼리만으로는 데이터가 줄어들지 않는다.
- 그래서 JPA에서 DISTINCT가 추가로 애플리케이션에서 중복 제거 시도.
- JPA가 같은 식별자를 가진 Team Entity를 result에서 제거해서 리스트를 반환.
- distinct 적용 후 결과 리스트의 사이즈를 찍어보면 중복이 제거된 것을 볼 수 있다.
resultList.size() = 2
team.getName() = 팀A team.getMembers().size() : 2
--> member = Member{id=3, username='회원1', age=0}
--> member = Member{id=4, username='회원2', age=0}
team.getName() = 팀B team.getMembers().size() : 1
--> member = Member{id=5, username='회원3', age=0}
7. 페치 조인과 일반 조인의 차이
- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음.
- JPQL
select t from Team t join t.members m where t.name = ‘팀A'
- SELECT하는 DATA는 결국 Team.
- member는 단순 join만하고 조회하진 않음.
- SQL
-
SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
-
- JPQL은 결과를 반환할 때 연관관계 고려하지 않음
- 단지 SELECT 절에 지정한 엔티티만 조회함
- 여기서는 팀 엔티티만 조회하고, 회원엔티티는 조회하지 않음.
- 페치 조인을 사용할 때 만 연관된 엔티티도 함께 조회(즉시로딩)
- 페치 조인은 객체 그래프를 SQL한 번에 조회하는 개념.
8. GitHub : 210816 FETCH JOIN1
'인프런 > [인프런] 자바ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
다형성 쿼리 (0) | 2021.08.16 |
---|---|
Fetch Join2 (0) | 2021.08.16 |
경로 표현식 (0) | 2021.08.16 |
JPQL 기본 함수 (0) | 2021.08.16 |
JPQL 조건식 - CASE (0) | 2021.08.16 |
Comments