1. 개요


JPA를 사용하면 Entity 객체를 중심으로 개발을 할텐데, 이때 문제는 검색 쿼리입니다. 따라서, 세심한 검색을 위해 쿼리를 짤 수 있어야 하고, JPA는 JPQL이라는 SQL을 추상화한 객체 중심 SQL을 제공합니다. JPQL은 테이블이 아닌 Entity 객체를 대상으로 검색합니다.
JPQL은 ANSI 표준에 나와있는 모든 SQL을 지원합니다.(SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN)

2. 프로젝션


SELECT절에 조회할 대상을 프로젝션이라고 하고, Entity, Embedded 타입, 스칼라 타입(기본 데이터 타입)이 있습니다.

SELECT m FROM Member m // 엔티티 프로젝션
SELECT m.team FROM Member m // 엔티티 프로젝션
SELECT m.address FROM Member m // 임베디드 타입 프로젝션
SELECT m.username, m.age FROM Member m // 스칼라 타입 프로젝션

Entity와 Embedded 타입 프로젝션은 클래스로 조회 값을 받을 수 있으니 명확합니다. 문제는 Scala 타입 프로젝션인데요,
Scala 타입 프로젝션은 다음과 같이 조회 값을 받을 수 있습니다.

1. Query 타입으로 조회

Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();

Object[] o = (Object[]) resultList.get(0); // Object 배열로 타입 캐스팅이 필요합니다.
System.out.println("username = " + o[0]);
System.out.println("age = " + a[1]);

2. new 키워드를 이용해 DTO에 담아 조회 (쿼리가 문자열이라 패키지명을 모두 명시해야 합니다.)

TypedQuery<MemberDTO> query =
	em.createQuery("SELECT new com.demo.dto.MemberDTO(m.username, m.age) FROM Member m");

3. 페이징 API


JPA는 페이징을 다음 두 API로 추상화합니다.

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult): 조회할 데이터 수
em.createQuery("SELECT m FROM Member m ORDER BY m.age DESC", Member.class)
        .setFirstResult(1)
        .setMaxResults(10)
        .getResultList();

4. JOIN


ON 절을 활용한 조인은 JPA 2.1부터 지원합니다.

1. JOIN 대상 필터링

String query = "SELECT m FROM Member m JOIN m.team t ON t.name = '회계팀'";
em.createQuery(query, Member.class).getResultList();

2. 연관 관계가 없는 Entity와 외부 JOIN. Hibernate 5.1부터 지원하고 JOIN절에 m.team 형태가 아닌 Team과 같이 엔티티명을 명시

String query = "SELECT m FROM Member m JOIN Team t ON m.username = t.name";
em.createQuery(query, Member.class).getResultList();

5. Subquery


기본 형태는 다음과 같습니다.

String query = 
	"SELECT m FROM Member m" +
    "WHERE m.age > (SELECT avg(m2.age) FROM Member m2)";

JPA 서브 쿼리의 한계는 다음과 같습니다.

  1. JPA 표준 스펙은 WHERE, HAVING절에서만 서브 쿼리 사용이 가능합니다.
  2. 표준은 아니지만 Hibernate는 SELECT절에서도 서브 쿼리를 지원합니다.
  3. FROM절에서의 서브 쿼리는 현재 JPQL에서 불가능 합니다. 따라서 JOIN으로 풀어서 해결해야 합니다.

6. 조건식


// 기본 CASE 식
SELECT
    CASE WHEN m.age <= 10 THEN '학생요금'
         WHEN m.age >= 60 THEN '경로요금'
         ELSE '일반요금'
    END
FROM Member m

// 단순 CASE 식
SELECT
    CASE t.name
        WHEN '팀A' THEN '인센티브110%'
        WHEN '팀B' THEN '인센티브120%'
        ELSE '인센티브105%'
    END
FROM Team t

7. JPQL 기본 함수


JPQL은 ANSI 표준 SQL 제공 함수를 똑같이 제공합니다.

8. 페치 조인(FETCH JOIN)


페치 조인은 SQL 조인의 종류가 아닌, JPQL에서 성능 최적화를 위해 제공하는 JPQL만의 JOIN입니다.
페치 조인을 사용하면 @ManyToOne, @OneToMany와 같이 연관 Entity 값을 한 번에 조회해옵니다.

Team teamA = new Team("회계팀"); em.persist(teamA);
Team teamB = new Team("개발팀"); em.persist(teamB);
Member member1 = new Member("홍길동", 20, teamA); em.persist(member1);
Member member2 = new Member("김길동", 21, teamA); em.persist(member2);
Member member3 = new Member("박길동", 22, 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) {
    // 이때 쿼리로 값을 채우기 때문에 N+1 문제가 발생합니다.
    System.out.println(member.getTeam().getName()); 
}

@ManyToOne(fetch = FetchType.LAZY)로 설정되어 있든 아니든간에 Team 엔티티를 채우기 위한 N+1 쿼리 문제가 발생합니다.
만약 다음과 같이 페치 조인을 사용한다면 연관 Entity를 조회할 때 한번에 세팅하기 때문에 쿼리 1번으로 모든게 해결됩니다.

String query = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class).getResultList();

그런데, 위 처럼 N쪽에서 페치 조인을 하는 것이 아니라, 1쪽에서 페치 조인을 하면 어떻게 될까요?

String query = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();

팀은 회계팀, 개발팀 2개인데, 3개의 결과값이 나오게 됩니다. DB를 생각해보면 1쪽에서 N을 조인하면 컬럼 수가 증가하기 때문입니다.
혹시 DB에서 중복된 결과를 제거하는 DISTINCT를 아시나요? JPA는 이 DISTINCT로 애플리케이션에서 중복된 Entity를 제거합니다.

String query = "select distinct t from Team t join fetch t.members";