JPQL ( Java Persistence Query Language ) - JPQL에 대해서 알아보자

JPQL ( Java Persistence Query Language )

JPA를 사용하면 Entity 객체 중심으로 개발할 수 있지만, DB를 검색할 때의 Query입니다.
검색할 때도 테이블이 아닌 Entity 객체를 대상으로 검색하는 JPQL을 사용

JPA는 SQL을 추상화한 객체 지향 쿼리 언어인 JPQL 제공
JPQL은 특정 DB SQL에 의존하지 않는다는 점이 특징이며, 추상화 되었더라고 보내질때는
SQL로 변환이 됩니다.


기본 문법

  • Entity와 속성은 대소문자를 구분합니다 ( ex . Member , username )
  • JPQL 키워드는 대소문자를 구분하지 않습니다. ( ex . SELECT , From )
  • 테이블 이름이 아닌 Entity 이름을 사용
  • 별칭을 필수로 사용해야 함 ( as 는 생략 가능 )
  • 반환 타입이 명확할 때는 TypedQuery , 그렇지 않을 때는 Query
  • TypedQuery<Member> query = em.createQuery("select m from Member as m", Member.class); Query query = em.createQuery("select m.name, m.age from Member m");
  • Query 결과가 하나 이상일 때는 getResultList()
    정확히 하나일때는getSingleResult()
  • TypedQuery<Member> query = em.createQuery("select m from Member as m", Member.class); Member singleResult = query.getSingleResult(); List<Member> resultList = query.getResultList();
  • 파라미터는 이름 기준으로 =: 를 사용해서 설정
  • TypedQuery<Member> query = em.createQuery("select m from Member as m where m.name=:name", Member.class) .setParameter("name", "memberA");

Projection

SELECT 절에 조회할 대상을 지정하는 것을 의미

그 대상으로는 Entity, Embedded타입, 스칼라 타입 등이 있음

DISTINCT 로 중복을 제거 가능

  1. **Object[] 타입으로 조회**
  2. List<Object[]> resultList = em.createQuery("select m.name, m.id from Member m") .getResultList(); for (Object[] o: resultList) { System.out.println("o = " + o[0] + ", " + o[1]); }
  3. DTO로 바로 조회
     String query = "select new hellojpa.MemberDTO(m.name, m.age) from Member m"
     List<MemberDTO> resultList = em.createQuery(query, MemberDTO.class)
         .getResultList();
    
     for (MemberDTO m: resultList) {
         System.out.println("m = " + m.getName() + ", " + m.getAge());
     }
    이 경우에는 패키지 명을 포함한 전체 클래스 명을 입력해야 하고, 순서와 타입이
    일치하는 생성자가 존재해야함
  4. public class MemberDTO { private String name; private int age; public MemberDTO(String name, int age) { this.name = name; this.age = age; } }

Paging

List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();

필요한 데이터만 나눠서 가져오는 것을 의미

조회 시작 위치화 조회할 데이터 수를 지정해주면 간단히 가능


Join

List<Member> resultList= em.createQuery("select m from Member m inner join m.team t", Member.class)
    .getResultList();    // 'inner'는 생략 가능
List<Member> resultList = em.createQuery("select m from Member m left outer join m.team t", Member.class)
    .getResultList();    // 'outer'는 생략 가능
List<Member> resultList = em.createQuery("select m from Member m, Team t where m.name = t.name", Member.class)
    .getResultList();

Inner Join, Outer Join, Theta Join을 할수 있고, ON 절을 활용한 Join도 가능

  • Join 대상 필터링
  • String query = "select m from Member m join m.team t on m.name = t.name"; List<Member> resultList = em.createQuery(query, Member.class) .getResultList();
  • 연관관계 없는 Entity Outer Join
  • String query = "select m from Member m left join Team t on m.name = t.name"; List<Member> resultList = em.createQuery(query, Member.class) .getResultList();

Sub Query

[NOT] EXISTS, ALL, ANY, SOME, [NOT] IN 등의 함수를 이용하여 Sub Query를 작성할 수 있음

표준 JPA에서는 WHERE, HAVING 절에서만 사용 가능하지만, Hibernate에서는 SELECT 절도 가능

FROM절의 Sub Query는 현재 JPQL에서 불가능

String query = "select m from Member m where m.team = any (select t from Team t)";
List<Member> resultList = em.createQuery(query, Member.class)
    .getResultList();

조건식

  • CASE
  • String query = "select " + "case when m.age > 10 then '학생요금'" + " else '일반요금'" + "end " + "from Member m";
  • COALESCE하나씩 조회한 후 null이 아니면 반환, null이면 두번째 파라미터 반환
  • String query = "select coalesce(m.name, '이름 없는 회원') from Member m";
  • NULLIF파라미터 두 값이 같으면 null을 반환, 다르면 첫번째 파라미터 반환
  • String query = "select nullif(m.name, 'memberA') from Member m";

JPQL 함수

  • 기본 함수
      String query = "select concat('a', 'b') from Member m";
      String query = "select upper(m.name) from Member m";
      String query = "select size(t.members) from Team t";
  • CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, MOD, SIZE,
    INDEX 등의 함수를 기본 제공
  • 사용자 정의 함수
      public class MyH2Dialect extends H2Dialect {
          public MyH2Dialect() {
              registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
          }
      }
      <properties>
          <property name="hibernate.dialect" value="hellojpa.MyH2Dialect"/>
      </properties>
      String query = "select function('group_concat', m.name) from Member m";
  • Hibernate의 경우 사용자 정의 함수를 미리 방언에 추가한 후 사용 가능

경로 표현식

. 을 찍어서 객체 그래프를 탐색하는 것을 의미

  • 상태 FieldEntity의 Field 중에서 단순히 값을 저장하기 위한 Field를 의미
    (Ex. [member.name](http://member.name) ) 경로 탐색의 끝으로, 추가적인 탐색 못함
  • String query = "select m.name from Member m"; List<String> resultList = em.createQuery(query, String.class) .getResultList();
  • 단일 값 연관 Field
      select m.*
      from Member m
      inner join Team t on m.team_id = t.team_id
    @ManyToOne, @OneToOne 연관관계인 경우로 탐색 대상이 Entity인 Field를 의미합니다. (ex. member.team) 묵시적으로 Inner Join이 발생하며, 추가적인 탐색을 할 수 있습니다.
  • String query = "select m.team from Member m"; // "select t from Member m join m.team t" 처럼 명시적 Join으로 표현 가능 // "select m.team.name from Member m" 처럼 추가 탐색 가능 List<Team> resultList = em.createQuery(query, Team.class) .getResultList();
  • Collection 값 연관 Field@OneToMany, @ManyToMany 연관관계인 경우로 탐색 대상이 Collection인 Field를 의미
    (ex. member.orders) 묵시적으로 Inner Join이 발생하며, 추가적인 탐색을 할 수 없음
    다만, FROM절에서 명시적 Join을 통해 별칭을 얻으면 그를 통해 탐색이 가능
  • String query = "select m.orders from Member m"; // "select m.orders.address from Member m" 와 같은 추가 탐색 불가능 // "select o.address" from Member m join m.orders o" 처럼 명시적 Join으로 추가 탐색 가능 List<Order> resultList = em.createQuery(query, Order.class) .getResultList();

실무에서는 가급적 묵시적 JOIN 대신에 명시적 JOIN을 사용 권장
JOIN은 SQL 튜닝에 중요한 포인트


Fetch Join

성능 최적화 관점에서, 실무에서 정말 중요한 부분

연관된 Entity 혹은 Collection을 SQL 한 번으로 함께 조회하는 기능

String query1 = "select m from Member m join m.team";    // 일반 join문
List<Member> resultList = em.createQuery(query1, Member.class)
    .getResultList();
for (Member m: resultList) {
    System.out.println("m = " + m.getName());
    // FetchType.LAZY이므로, 영속성 Context에는 아직 team을 위한 정보가 없는 상태
    System.out.println("t = " + m.getTeam().getName());
    // team 관련된 정보 요청이 들어오면 그때서야 SELECT query를 보내 정보를 가져옴
}

String query2 = "select m from Member m join fetch m.team";        // fetch join문
List<Member> resultList = em.createQuery(query2, Member.class)
    .getResultList();
for (Member m: resultList) {
    System.out.println("m = " + m.getName());
    // join fetch했므로, 영속성 Context에는 member 그리고 연관된 team 정보가 모두 있는 상태
    System.out.println("t = " + m.getTeam().getName());
    // 1차 캐시에서 정보를 가져옴
}

만약 규모가 큰 Application에서 일반 JOIN문으로 한 Entity와 연관된 Entity 정보를 가져온다면, N+1 문제가 발생할 수 있음

Fetch Join을 사용하면 연관된 Entity 정보들을 한 번에 가져오므로 N+1 문제 방지 가능

즉시 로딩 속성으로 Entity 조회

distinct 사용 예시

String query1 = "select t from Team t join fetch t.members where t.name='teamA'";
List<Team> resultList = em.createQuery(query1, Team.class)
    .getResultList();
for (Team t: resultList) {
    System.out.println("t = " + t.getId() + ": " + t.getName());
    for (Member m: t.getMembers()) {
        System.out.println("    m = " + m.getName());
    }
}
// 각 team에 속한 member 수만큼 반복해서 결과 출력

String query2 = "select distinct t from Team t join fetch t.members where t.name='teamA'";
List<Team> resultList = em.createQuery(query2, Team.class)
    .getResultList();
for (Team t: resultList) {
    System.out.println("t = " + t.getId() + ": " + t.getName());
    for (Member m: t.getMembers()) {
        System.out.println("    m = " + m.getName());
    }
}
// 각 team 한 번씩만 결과 출력

SQL에서 DISTINCT는 중보된 결과를 제거하는 명령이라면, JPA에서는 SQL에 DISTINCT를 추가하고 Application에서 중복 Entity를 제거 해줌

Fetch Join의 한계

  • Fetch Join 대상에 별칭을 줄 수 없음. Hibernate에서는 가능하지만, 가급적 사용X
  • 둘 이상의 Collection은 Fetch Join 할 수 없음
  • Collection을 Fetch Join하면 Paging API를 사용할 수 없음

만약 여러 테이블을 JOIN해서 Entity가 가진 형태가 아닌 다른 결과가 필요하다면,
Fetch Join보다는 일반 Join을 사용하고 그에 맞는 DTO로 반환하는 것이 효과적


다형성 Query

  • Type
      String query = "select i from Item i where type(i) IN (Book, Movie)";
  • 조회 대상을 특정 자식으로 한정할 때 사용
  • Treat
      String query = "select i from Item i where treat(i as Book).author = 'joon'";
  • 자바의 TypeCasting과 유사한 개념, 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰때 사용.

Named Query

미리 정의해서 이름을 부여해 주고 사용하는 JPQL로, 정적인 Query
Application 로딩 시점에 초기화 후 캐시에서 재사용되어 성능 상 이점이 있고,
로딩 시점에 Query를 검증해줌

@Entity
@NamedQuery(
        name = "Member.findByName",
        query = "select m from Member m where m.name = :name")
public class Member {
    ...
}
List<Member> resultList = em.createNamedQuery("Member.findByName", Member.class)
    .setParameter("name", "memberA")
    .getResultList();

벌크 연산

Query 한번으로 테이블의 여러 Entity를 변경할 때 사용.
대량의 Field 값 갱신이 필요한 경우에 일반 UPDATE문으로 하면 엄청 많은 UPDATE SQL 발생
벌크 연산으로 한번의 Query로 가능

String query = "update Member m " +
    "set m.age = age * 2 " +
    "where m.age > 0";
int resultCount = em.createQuery(query)
    .executeUpdate();

UPDATE, DELETE를 지원하며, 실행 결과는 영향받은 Entity 수를 반환합니다.
벌크 연산은 영속성 Context를 무시하고 DB에 직접 Query를 보내는 점을 유의해서 사용