Study/spring

자바 ORM 표준 JPA 프로그래밍(12) - JPQL 기본 문법과 파라미터 바인딩, 프로젝션, 페이징

유경호 2021. 1. 6. 22:16
반응형

 

JPQL(Java Persistence Query : Language)

  • JPQL은 객체지향 쿼리 언어이다. 테이블 대상으로 쿼리하는 것이 아닌 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화하여 특정 데이터베이스 SQL에 종속적이지 않다.
  • JPQL은 결국 SQL로 변환되어 실행한다.

JPQL 문법

  • select 문 :: =

    select_절
    from_절

    [where_절]

    [groupby_절]

    [having_절]

    [orderby_절]

  • update_문 :: = update_절 [where_절]

  • delete_문 :: = delete_절 [where_절]

SELECT문은 다음과 같이 사용한다.

SELECT m FROM Member AS m where m.username = 'Hello'

  • 대소문자 구분

    → 엔티티와 속성은 대소문자를 구분한다. Member, username과 같은 엔티티명과 속성명이 이에 해당한다. 반면에 SELECT, FROM, AS 등과 같은 JPQL 키워드는 대소문자를 구분 하지 않는다.

  • 엔티티 이름

    →JPQL에서 사용한 Member는 클래스 명이 아닌 엔티티 명이다. @Entity(name = "xxx")로 지정된 값을 의미한다. 엔티티 명을 지정하지 않으면 클래스명을 기본값으로 한다.

  • 별칭 필수

    → Member AS m에 m이라는 별칭을 준 것을 볼 수 있다. JPQL은 별칭이 필수로 사용돼야 한다. 별칭을 사용하지 않으면 문법 오류를 발생시킨다. as는 생략 가능

집합과 정렬

  • COUNT(), SUM(), AVG(), MAX(), MIN()
  • GROUP BY, HAVING
  • ORDER BY

TypeQuery와 Query

  • TypeQuery: 반환 타입이 명확할 때 사용
  • Query: 반환 타입이 명확하지 않을 때 사용
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
Query query = em.createQuery("select m.username, m.age from Member m");
  • em.createQuery()의 두 번째 파라미터에 반환할 타입을 지정하면 TypeQuery를 반환하고 지정하지 않으면 Query를 반환한다.
  • 조회 대상이 Member 엔티티라면 조회 대상 타입이 명확하기 때문에 TypeQuery를 사용할 수 있다.
  • 조회 대상이 String 타입인 회원 이름과 Integer 타입인 나이처럼 조회 대상 타입이 명확하지 않으면 Query 객체를 사용해야 한다.
    • Query 객체는 SELECT 절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환하고 대상이 하나이면 Object를 반환한다.

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환. 결과가 없으면 빈 리스트 반환
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException

파라미터 바인딩

이름 기준 파라미터

TypedQuery<Member> query = em
        .createQuery("select m from Member m where m.username = :username", Member.class);

query.setParameter("username", "Kyeongho");
List<Member> resultList = query.getResultList();

→ JPQL에 :username이라는 이름 기준 파라미터를 정의하고 query .setParameter()에서 username이라는 이름으로 파라미터를 바인딩한다.

아래와 같이 메소드 체인 방식으로 연속해서 작성할 수도 있다.

List<Member> resultList = em
        .createQuery("select m from Member m where m.username = :username", Member.class)
        .setParameter("username", "Kyeongho")
        .getResultList();

위치 기준 파라미터

List<Member> resultList = em
        .createQuery("select m from Member m where m.username = ?1", Member.class)
        .setParameter(1, "Kyeongho")
        .getResultList();

→ 위치 기준 파라미터(Positional parameters)를 사용하려면 ? 다음에 위치 값을 주면 된다.

 

위치 기준 파리미터 주의점

위치(순서) 기준보다는 이름 기준의 바인딩을 추천함. 위치 기준의 바인딩을 사용하면 추후에 요소가 추가되면 밀리거나 땡겨질 수가 있기 때문

프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라 한다. [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다.

  • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)

  • 엔티티 프로젝션

    • SELECT m FROM Member m
    • SELECT m.team FROM Member m
      • 위 처럼만 실행하면 묵시적 조인 쿼리가 나가는데, 이보다는 조인을 JPQL에 명시해주는 것을 권장함
    • 조회된 엔티티는 영속성 컨텍스트에서 관리된다.
  • 임베디드 타입 프로젝션

    • SELECT m.address FROM Member m

      • 임베디드 타입이 속해있는 Entity를 명시해줘야함 (예: Member)
    • 아래 코드처럼 Order 엔티티가 시작점이며, 이렇게 엔티티를 통해서 임베디드 타입을 조회할 수 있다. (! Order 엔티티와 매핑된 테이블은 Orders다. 엔티티 이름으로 넣어줘야함)

      String query = "SELECT o.address FROM Order o";
      List<Address> addresses = em.createQuery(query, Address.class)
                                                            .getResultList();
  • 스칼라 타입 프로젝션

    • SELECT m.username, m.age FROM Member m

    • DISTINCT로 중복 제거

      SELECT DISTINCT username FROM Member m

    • 통계 쿼리와 같은 것들이 주로 스칼라 타입으로 조회된다.

여러 값 조회

엔티티를 대상으로 조회하면 편리하나, 꼭 필요한 데이터들만 선택해서 조회할 때가 있다. 프로젝션에 여러 값을 선택하면 TypeQquery를 사용할 수 없고 대신에 Query를 사용해야 한다.

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

Iterator iterator = resultList.iterator();
while(iterator.hasNext()) {
    Object[] = row = (Object[]) iterator.next();
    String username = (String) row[0];
    int age = (Integer) row[1];
}

제네릭에 Object[]를 사용하여 아래와 같이 좀 더 간결하게 코드를 짤 수 있다.

List<Object[]> resultList = 
    em.createQuery("SELECT m.username, m.age FROM Member m")
        .getResultList();

for(Object[] row : resultList) {
    String username = (String) row[0];
    int age = (Integer) row[1];
}

스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.

List<Object[]> resultList = 
    em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
        .getResultList();

for(Object[] row : resultList) {
    Member member = (Member) row[0];    // 엔티티
    Product product = (Product) row[1]; // 엔티티
    int orderAmount = (Integer) row[2]; // 스칼라
}

 

NEW 명령어로 조회

여러 스칼라 타입의 속성을 프로젝션해서 타입을 지정할 수 없기에 TypeQuery를 사용할 수 없다. 그래서 Object[]를 반환받았다. 실제 어플리케이션 개발시에는 Object[]를 직접 사용하지 않고 아래와 같이 UserDTO처럼 의미 있는 객체로 변환해서 사용할 것이다.

List<Object[]> resultList = 
    em.createQuery("SELECT m.username, m.age FROM Member m")
        .getResultList();

List<UserDTO> userDTOs = new ArrayList<UserDTO>();
for(Object[] row : resultList) {
    UserDTO userDTO = new UserDTO((String) row[0], (Integer) row[1]);
    userDTOs.add(userDTO);
}

하지만 이런 객체 변환 작업 또한 지루함의 연속이다. 아래와 같이 NEW 명령어를 사용해 프로젝션할 수 있다.

TypedQuery<UserDTO> query = 
    em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) 
    FROM Member m". UserDTO.class);

List<UserDTO> userDTOs = query.getResultList();

→ SELECT 다음에 NEW 명령어를 사용하면 반환 받을 클래스를 지정할 수 있다. 그러면 JPQL 조회 결과를 이 클래스의 생성자에 넘겨줄 수 있다. 그렇기에 순서와 타입이 일치하는 생성자 필요하다. 그리고 NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있다.

또한 지금은 new 명령어 뒤에 키지 명을 포함한 전체 클래스 명 모두 기입해줘야 하지만 QueryDSL을 사용하면 클래스를 Import하여 쓸 수 있기 때문에 위 작업도 생략할 수 있게 된다.

 

페이징 API

  • JPA는 페이징을 다음 두 API로 추상화하여 데이터베이스간 페이징 문법 차이를 신경쓰지 않아도 됨
  • setFirstResult(int startPosition): 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult): 조회할 데이터 수
반응형