본문 바로가기
프로그래밍/Back-end

JPA 쿼리 패치 조인 JPQL FETCH JOIN

by @GodWin 2024. 10. 24.

-

 

-
안녕하세요? 오늘은 JPA에서 JPQL 패치(FETCH) 조인 에 대해 알아보도록 하겠습니다.


 

-

페치 조인 

(FETCH JOIN)

: SQL 조인 종류가 아니라, JPQL에서 성능최적화를 위해 제공

: 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회

JPQL SELECT e FROM 엔티티 e JOIN FETCH e.연관관계객체 s
SQL SELECT t1.*, t2.* FROM 테이블1 as t1
INNER JOIN 테이블2 as t2
ON t1.key = t2.key


+
예로 들어서, Member 엔티티와, Team 엔티티가 존재하고, Member는 Team과 다대일 [N:1] 연관관계라고 가정했을 때

멤버 엔티티 생성)

@Entity
public class Member {
  
  @Id @GeneratedValue
  @Column(name = "MEMBER_ID")
  private Long id;
  
  @Column(name = "MEMBER_NAME")
  private String name;
  
  @ManyToOne
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  
  ...
  
}


팀 엔티티 생성)

@Entity
public class Team {
  
  @Id @GeneratedValue
  @Column(name = "TEAM_ID")
  private Long id;
  
  @Column(name = "TEAM_NAME")
  private String name;
  
  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();
  
  ...
  
}


샘플 코드)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("유닛명");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

Team teamA = new Team();
team.setName("teamA");

em.persist(teamA);

Team teamB = new Team();
team.setName("teamB");

em.persist(teamB);

Member memberA = new Member();
memberA.setName("memberA");
memberA.setTeam(teamA);
em.persist(memberA);

Member memberB = new Member();
memberB.setName("memberB");
memberB.setTeam(teamA);
em.persist(memberB);

Member memberC = new Member();
memberC.setName("memberC");
memberC.setTeam(teamB);
em.persist(memberC);

em.flush();
em.clear();

String jpql = "SELECT m FROM Member m"

List<Member> result = em.createQuery(jpql, Member.class)
      .getResultList();

for (Member member : result) {
  System.out.println("member = " + member.getName()
                   + ", " + member.getTeam().getName()); // 플래시 실행
}

// 결과 : Member 조회 쿼리 1번 + Team 조회 쿼리 2번
// member 데이터 조회 (SQL) : memberA, memberB, memberC
// memberA, teamA (SQL) : teamA 데이터 조회
// memberB, teamA (1차캐시) : teamA 1차캐시 데이터 조회
// memberC, teamB (SQL) : teamB 데이터 조회

jpql = "SELECT m FROM Member m JOIN FETCH m.team";

result = em.createQuery(jpql, Member.class)
      .getResultList();

for (Member member : result) {
  System.out.println("member = " + member.getName()
                   + ", " + member.getTeam().getName());
}

// 결과 : JOIN으로 한방에 데이터 전체 조회
// member 데이터 조회 (SQL) : memberA, memberB, memberC
// memberA, teamA (1차캐시) : teamA 1차캐시 데이터 조회
// memberB, teamA (1차캐시) : teamA 1차캐시 데이터 조회
// memberC, teamB (1차캐시) : teamB 1차캐시 데이터 조회

tx.commit();


DB 결과) - 멤버 테이블

MEMBER_ID MEMBER_NAME TEAM_ID (FK)
1 memberA 1
2 memberB 1
3 memberC 2


DB 결과) - 팀 테이블

TEAM_ID TEAM_NAME
1 teamA
2 teamB



※ 지연로딩을 해도, 패치 조인이 항상 우선


컬렉션 페치 조인


일대다 [1:N] 관계에서 사용

위의 예제에서, Team에서 Member의 데이터를 조회한다고 가정했을 때

샘플코드)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("유닛명");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

String jpql = "SELECT t FROM Team t JOIN FETCH t.members"

List<Member> result = em.createQuery(jpql, Member.class)
      .getResultList();

for (Team team : result) {
  System.out.println("team = " + team.getName()
                   + ", " + team.getMembers().getName()); // 플래시 실행
}

// 결과 : 중복 데이터 조회 -> N + 1 문제 발생 ★★
// teamA, memberA
// teamA, memberB
// teamB, memberC

tx.commit();


+
N+1 문제가 발생
> 중복 결과 데이터 발생
DISTINCT 사용
>> SQL에 추가 + App에서 중복 제거

샘플코드)

String jpql = "SELECT DISTINCT t"
            + "FROM Team t"
            + "JOIN FETCH t.members"
            + "WHERE t.name = 'teamA'"

List<Member> result = em.createQuery(jpql, Member.class)
      .getResultList();

for (Team team : result) {
  System.out.println("team = " + team.getName()
                   + ", " + team.getMembers().size());
}

// 결과 : 중복 제거 -> 1건 반환
// teamA, 2



+++

Fetch 조인과 일반 조인의 차이점


: JPQL은 결과를 반환할 때, 연관관계를 고려하지 않음
> 단지 SELECT 절에 지정한 엔티티만 조회
>> Team 엔티티만 조회하고, Member 엔티티는 조회X
: 페치 조인을 사용할 때만 연관된 엔티티도 함꼐 조회 (즉시로딩)
: 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념


+++
페치 조인의 한계
: 페치 조인 대상에는 별칭 부여 불가
: 둘 이상의 컬렉션은 사용 불가
: 페이징 API 사용 불가
> 단일 값 연관 필드(일대일, 다대일)들은 가능


페이조인은 객체 그래프를 유지할 때 사용하면 효과적

여러 테이블을 조인해서,
엔티티가 가진 모양이 아닌,
전혀 다른 결과를 내야한다면,
페이조인 보다는,
일반 조인을 사용하고,
필요한 데이터만 조회해서,
DTO로 반환하는 것이 효과적

 

 


※ JPA에서의 쿼리 방법에 대해서는, 아래 포스팅을 참조 부탁드리겠습니다.

https://logger-debug.tistory.com/entry/JPA-QueryDSL-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8CSQL-JDBC-MyBatis

 

JPA QueryDSL 네이티브SQL JDBC MyBatis

- -안녕하세요? 오늘은 JPA의 다양한 쿼리 방법에 대해서 알아보도록 하겠습니다. JPA에서는,JPQL ★★★JPQ CriteriaQueryDSL ★★★네이티브 SQLJDBC 직접 사용(MyBatis, SpringJdbcTemplate ...)등등 다양한 쿼

logger-debug.tistory.com

https://logger-debug.tistory.com/entry/JPA-%EC%BF%BC%EB%A6%AC-JPQL-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%A1%B0%EC%9D%B8-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC

 

JPA 쿼리 JPQL 페이징 조인 서브쿼리

- -안녕하세요? 오늘은, JPA의 쿼리 방법 중 하나인, JQPL에 대해 알아보도록 하겠습니다.※ JPA에서의 쿼리 방법에 대해서는, 아래 포스팅을 참조 부탁드리겠습니다.https://logger-debug.tistory.com/entry/JP

logger-debug.tistory.com

https://logger-debug.tistory.com/entry/JPA-JPQL-%ED%83%80%EC%9E%85-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-%EA%B8%B0%ED%83%80%EC%8B%9D-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%A1%B0%EA%B1%B4%EC%8B%9D%EA%B3%BC-%EA%B8%B0%EB%B3%B8-%ED%95%A8%EC%88%98

 

JPA JPQL 타입 표현식과 기타식 그리고 조건식과 기본 함수

- -안녕하세요? 오늘은 JPA의 JPQL 타입 표현식과 기타식 그리고 조건식과 기본 함수 에 대해서 알아보도록 하겠습니다.※ JPA에서의 쿼리 방법에 대해서는, 아래 포스팅을 참조 부탁드리겠습니다.

logger-debug.tistory.com

https://logger-debug.tistory.com/entry/JPA-JPQL-%EA%B2%BD%EB%A1%9C-%ED%91%9C%ED%98%84%EC%8B%9D

 

JPA JPQL 경로 표현식

- -안녕하세요? 오늘은 JPA에서 JPQL의 경로 표현식 에 대해 알아보도록 하겠습니다.※ JPA에서의 쿼리 방법에 대해서는, 아래 포스팅을 참조 부탁드리겠습니다.https://logger-debug.tistory.com/entry/JPA-Quer

logger-debug.tistory.com

https://logger-debug.tistory.com/entry/JPA-%EC%BF%BC%EB%A6%AC-JPQL-%EB%B2%8C%ED%81%AC-%EC%97%B0%EC%82%B0

 

JPA 쿼리 JPQL 벌크 연산

- -안녕하세요? 오늘은 JPA의 JPQL 중 벌크 연산에 대해 알아보도록 하겠습니다.-벌크 연산: 쿼리 한번으로, 여러 테이블 로우 변경: executeUpdate의 결과는 영향받은 엔티티 수를 반환: Update / Delete

logger-debug.tistory.com

 


오늘은 JPA에서 JPQL 패치 조인 에 대해서 알아보았습니다.
그럼 오늘도 즐거운 하루 되시길 바라겠습니다.