JPA

[JPA] 26. Fetch 조인 - 기본

inuma 2021. 2. 11. 17:41
프로젝트 git url github.com/rtef23/JpaStudy
필요 1. docker
2. docker-compose
프로젝트 정보 mysql : 5.7 - docker image
java : 1.8

 

Fetch Join

  • SQL 조인 종류가 아니다
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 쿼리 한번에 함께 조회하는 기능(즉시 로딩과 유사)
  • join fetch 명령어 사용
  • ::= [INNER | LEFT (OUTER) | RIGHT (OUTER)] JOIN FETCH {조인 경로}

 

 

//[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

 

List<Team> teams = Arrays.asList(
  new Team("team-name1"),
  new Team("team-name2"),
  new Team("team-name3")
);

teams.forEach(entityManager::persist);

IntStream.rangeClosed(0, 9)
  .forEach(
    (number) -> {
      Member member = new Member("name-" + number, number);
      Team selectedTeam = teams.get((int) (Math.random() * teams.size()));

      member.changeTeam(selectedTeam);

      entityManager.persist(member);
  });

entityManager.flush();
entityManager.clear();

String jpqlQuery = "select m from Member m";
List<Member> results = entityManager.createQuery(jpqlQuery, Member.class).getResultList();

for(Member member : results){
	System.out.println(member.getUsername() + ",\n\t" + member.getTeam().getName());
}

  • Member 엔티티의 갯수만큼 Team을 조회하는 쿼리가 실행되게 된다.
    (N + 1 문제 발생)
    (영속성 컨텍스트에서 관리하고 있는 Team의 경우, 영속성 컨텍스트에서 관리하는 Team 엔티티를 반환한다.)

 

String jpqlQuery = "select m from Member m join fetch m.team";
List<Member> results = entityManager.createQuery(jpqlQuery, Member.class).getResultList();

for(Member member : results){
	System.out.println(member.getUsername() + ",\n\t" + member.getTeam().getName());
}

  • 최초에 Member를 조회할 때부터 Team을 같이 조회하기 때문에 Team을 조회하는 추가 쿼리가 발생하지 않는다.
    (이 시점에서 Team은 프록시 객체가 아닌 실제 엔티티가 들어가게 된다.)

 

컬렉션 페치 조인

//[JPQL]
SELECT t
  FROM Team t
  JOIN FETCH t.members


//[SQL]
SELECT t.*, m.*
  FROM Team t
  INNER JOIN Member m
    ON t.id = m.team_id
List<Team> teams = entityManager.createQuery("select t from Team t join fetch t.members", Team.class).getResultList();

teams.forEach((team) -> {
	System.out("team name : " + team.getName() + "\tsize : " + team.getMembers().size());
});

/*
  //일대다의 관계에서, team-1에 속한 회원이 2명이고, team-2에 속한 회원이 1명의 경우 result
  | member_id | team_id |
  ========================
  | member-1  | team-1  |
  | member-2  | team-1  |
  | member-3  | team-2  |
  
  //Java 출력
  //이 때, team-1인 엔티티가 2개의 row로 출력이 되는데 2개의 엔티티는 동일한 엔티티 객체이다.(Member 엔티티도 동일하다.)
  team name : team-1  size : 2
  team name : team-1  size : 2
  team name : team-2  size : 1
*/

 

페치 조인과 DISTINCT

  • JPQL의 DISTINCT는 2가지 역할을 수행한다.
    • SQL에 DISTINCT 키워드 추가
    • 애플리케이션에서 중복 제거
//[JPQL]
SELECT DISTINCT t
  FROM Team t
  JOIN FETCH t.members
  
  
//[SQL]
SELECT distinct
     , t...
     , m...
  FROM Team t
  INNER JOIN Member m
    ON t.id = m.team_id

/*
  SQL에서의 distinct는 row가 완전히 동일한 경우에만 줄여지게 된다.
  | team_id | member_id |
  =======================
  | team-1  | member-1  |
  | team-1  | member-2  |
  | team-2  | member-3  |
  
  이를 해결하기 위해서 JPA에서는 같은 식별자를 가진 엔티티를 제거하는 추가 작업을 수행한다.
  [Team(id : team-1, members : [Member(id : member-1), Member(id : member-2)]),
   Team(id : team-2, members : [Member(id : member-3)])]
*/

 

일반 조인과 페치 조인의 차이

  • 일반조인의 경우, 실행시 연관된 엔티티를 조회하지 않는다.
  • 페치조인을 사용하는 경우에만, 연관된 엔티티도 함께 조회한다.
  • 객체 그래프를 쿼리 한번에 조회하는 개념
//일반 조인 예시
//[JPQL]
SELECT t
  FROM Team t
  INNER JOIN t.members
  
//[SQL]
SELECT t.*
  FROM Team t
  INNER JOIN Member m
    ON t.id = m.team_id
//페치 조인 예시
//[JPQL]
SELECT t
  FROM Team t
  JOIN FETCH t.members
  
//[SQL]
SELECT t.*, m.*  //연관된 엔티티 조회
  FROM Team t
  INNER JOIN Member m
    ON t.id = m.team_id