저번 JPQL 강의에 이어서 JPQL에 대해서 알아보자.

경로 표현식

우리가 흔히 SQL 문법을 쓸 때 .을 찍어 탐색하는 것을 말한다.

  • 상태 필드 : 단순히 값을 저장하기 위한 필드이다.
    • 경로 탐색의 끝이며, 더 이상 탐색을 진행하지 않는다.
    • m.username과 같은 필드이다.
  • 연관 필드 : 연관관계를 위한 필드이다.
    • 단일 값
      • @ManyToOne, @OneToOne 처럼, 대상이 단일 Entity일 때.
      • SELECT m.team FROM Member m과 같이 묵시적으로 INNER JOIN이 발생한다.
        • 따라서 조심해서 사용을 해야한다 !
      • 상태 필드와 다르게, 더 깊게 탐색이 가능하다.
    • 컬렉션 값
      • @OneToMany 처럼, 대상이 Collection 값일 때
      • 단일 값과 같이 묵시적 INNER JOIN이 발생한다.
      • 상태 필드와 같이, 값이 많아서 더 깊게 탐색이 불가능하다.
      • 만약, 더 탐색을 하고 싶다면 명시적으로 JOIN을 하자!
        • SELECT m.name FROM Team t JOIN t.members m과 같이.

웬만하면 묵시적 조인은 절대 쓰지말것!!
참고로, 묵시적 조인은 INNER JOIN만 가능하다.

Fetch JOIN

SQL에는 없는, JPQL만의 Fetch JOIN에 대해 알아보자.
JPQL에서 성능 최적화를 위해 제공하는 기능으로,
연관된 엔티티나 Collection을 SQL 한번으로 함께 조회하는 기능이다.
실무에서 정말 많이 사용한다!

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());
}

위와 같이 Query문을 작성하게 되면, N+1 문제가 불가피 해진다!
회원 100명이 각자 다른 팀에 소속해서 Query를 조회할 경우, 총 101번의 Query가 나간다는 것이다.

하지만, JOIN Fetch를 사용하는 경우엔, Query 한 번에 모든 Data를 조회한다.
한 번에 Context에 영속시키기 때문에, 더 이상의 Query는 보내지 않는다.

Fetch Join은 일반 Join과 다르게 연관 Entity를 싹 다 한번에 가져오는 것이다.
일반 Join은 내가 Select한 Entity만 가져오고, 나머지가 필요하면 Query를 또 날려야 한다.

Fetch JOIN과 Distinct

원래 Fetch Join은 정규 스펙 상, Query의 모든 결과를 가져와야 한다.
예를 들어 아래의 Query문을 실행한다고 생각해보자.
SELECT t FROM team t join fetch t.members

그럼, Team table과 해당 Team에 속한 Member Table이 Join되어 한 Table에 들어온다.
이 때 만약 Team 1에 있는 회원이 3명이라면, Team의 이름을 조회하면 같은 답이 3번 나온다.

왜일까? Join한 Table에는 Member가 3명이니, Team에 해당하는 Column이 3개가 되기 때문이다.
이를 해결하기 위해서는, SELECT DISTINCT를 이용한다.

일대다 관계 를 Join 했을 때 발생하는 Data 뻥튀기 에 대한 대처법이다.

Fetch JOIN의 한계

만능처럼 보이는 Fetch Join도 한계는 존재한다.

  • Fetch join 대상은 별칭(Alias)를 주지 않는 것이 관례이다.
    • 100개의 Member를 조회한다면, 그 중 5명만 따로 조회하는 건 위험하다.
      • 왜냐하면, Fetch join은 이미 Collection의 값을 모두 가져옴을 상정하고 있다.
    • 그냥 Select 쿼리를 5개 보내는 게 훨씬 안전하고 효율적이다!
  • 둘 이상의 Collection은 Fetch join 할 수 없다.
  • Collection을 Fetch join하면, 페이징 API를 사용할 수 없다.
    • 일대다 관계에서 페이징을 해버리면 어떻게 될까?
      • 해당 Table에는 페이징 한 Row 밖에 없다고 DB가 인식하게 되어버린다.
    • 일대일, 다대일 같은 단일 값 연관 필드는 페이징이 가능하다.
    • Hibernate는 경고 로그를 남기고 메모리에서 페이징한다.
      • 예를 들어, Row가 100만건이 있다면 모두 메모리로 퍼올린 뒤 페이징한다.
      • 이는 매우 위험하다!
    • 이를 해결하기 위해서는, @BatchSize(size = 100) Annotation을 이용한다.
      • Properties 속성에 default_batch_fetch_size를 정해서 전역 설정도 가능하다.
    • 또는, DTO를 생성해서 Query를 직접 짜는 것이다.

Entity의 직접 사용

JPQL에서 Entity를 직접 사용하면, SQL에서는 해당 Entity의 PK를 사용한다.

Query = SELECT m FROM Member m WHERE m = :member
em.createQuery(Query, Member.class).setParameter("member", member)

위와 같이 Query를 짰다면, SQL문을 보면 member.id 로 Data를 추림을 알 수 있다.

Query = SELECT m FROM Member m WHERE m.team = :team
em.createQuery(Query, Member.class).setParameter("team", teamA)

위와 같이 짰다면, Table 밖의 값이므로 team.id 로 Data를 추림을 알 수 있다.
즉 PK가 아닌, 매핑된 FK를 통해서 Join을 하게 된다.

Named Query

@NamedQuery Annotation을 통해 정적 쿼리를 미리 선언해놓을 수 있다.

@Entity
@NamedQuery(name = Member.findByUsername",
            query = "SELECT m FROM Member m WHERE m.username = :username")
public class Member{
}

em.createNamedQuery("Member.findByUsername", Member.class)
          .setParameter("username", "Member1")
          .getResultList();

위와 같이 선언해놓은 정적 쿼리를 가져다 쓸 수 있다.