JPQL

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
  • SQL을 추상화해서 특정 db SQL에 의존하지 않는다.

SQL이 db 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다.

JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 db를 조회한다.

그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.

JPQL을 한마디로 정의하면 객체지향 SQL이다. 

 

JPA가 공식 지원하는 기능

  • JPQL
  • Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API,빌더 클래스모음
  • 네이티브 SQL: JPA에서 JPQL대신 직접 SQL을 사용할수 있음.

공식지원은 아니지만 알아야 할 기능

  • QueryDSL: Criteria 쿼리 처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스모음,비표준 오픈소스 프레임워크
  • JDBC 직접사용, MyBatis같은 SQL 매퍼 프레임워크 사용: 필요하면 JDBC직접 사용할 수 있다.

가장 중요한건 JPQL이다. Criteria나 QueryDSL은 JPQL을 편하게 작성하도록 도와주는 빌더 클래스.

따라서 JPQL을 이해해야 나머지도 이해할 수 있다. 

JPQL

SELECT문

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

 

  • 대소문자 구분: 엔티티와 속성은 대소문자를 구분한다. 예를 들어 Member,username은 대소문자 구분. 반면 SELECT,FROM,AS같은 JPQL 키워드는 대소문자 구분 X
  • 엔티티 이름 JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명을 지정하지 않으면 클래스명을 기본값으로 사용한다. 
  • 별칭은 필수: Member AS m을 보면 Member에 m이라는 별칭을 주었다. JPQL은 별칭을 필수로 사용해야 한다. 따라서 SELCET username FROM Member m 은 잘못된 문법. m.username으로 고쳐야 한다.AS 생략할 수 있다.

JPQL 문법

1
2
3
4
5
6
7
8
9
10
11
selcet_문:: =
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]
 
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
 
cs

 

TypeQuery, Query

작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query객체를 사용한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//타입쿼리 사용
TypeQuery<Member> query=
            em.createQuery("SELECT m FROM Member m",Member.class);
List<Member> resultList=query.getResultList();
for(Member member : resultList){
    System.out.println("member =" +member);
}
 
//Query 사용
Query query=
    em.createQuery("SELECT m.username, m.age from Member m");
List resultList=query.getResultList();
for (Object o : resultList){
    Object[] result=(Object[]) o; // 결과가 둘 이상이면 Object[] 반환
    System.out.println("username="+result[0]);
    System.out.println("age ="+result[1]);
}
 
cs

 

결과 조회 : query.getResultList(),결과를 예제로 반환한다. 결과 없으면 빈 컬렉션반환.

 

10.2.2 파라미터 바인딩

JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다.

 

이름 기준 파라미터 

이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다.

이름 기준 파라미터는 앞에 :를 사용한다. 

1
2
3
4
5
6
7
String usernameParam="USER1";
 
TypedQuery<Member> query=
        em.createQuery("SELECT m FROM Member m where m.username = :username",Member.class);
 
query.setParameter("username",usernameParam);
List<Member> resultList= query.getResultList();
cs

JPQL을 보면 :username이라는 이름 기준 파라미터를 정의하고 query.setParameter()에서 username이라는 이름으로 파라미터를 바인딩한다. 참고로 JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어서 다음과 같이 연속해서 작성할 수 있다.

 

위치 기준 파라미터

1
2
3
4
List<Member>members= em.createQuery("SELECT m FROM Member m where m.username = ?1",Member.class)
.setParameter(1,usernameParam)
.query.getResultList();
 
cs

 위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하ㅏㄷ.

 

파라미터 바인딩 방식을 사용하지 않고 직접 JPQL을 만들면 위험하다. (SQL 인젝션,성능 이슈)

파라미터 바인딩 방식은 선택이 아닌 필수다.

 

10.2.3 프로젝션

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

프로젝션 대상은 엔티티,임베디드 타입,스칼라 타입이 있다. 

 

엔티티 프로젝션

SELECT m FROM Member m

SELECT m.team FROM Member m

 

쉽게 생각하면 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야 하는 SQL과는 차이가 있다.

참고로 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

 

임베디드 타입 프로젝션

JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 다음은 임베디드 타입인 Address를 조회의 시작점으로 사용해서 잘못된 쿼리다.

 

String query="SELECT a FROM Address a";

다음 코드에서 Order 엔티티가 시작점이다. 이렇게 엔티티를 통해서 임베디드 타입을 조회할 수 있다.

String query="SELECT o.address FROM Order o";

List<Address> addresses = em.createQuery(query,Address.class).getResultList();

 

임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

 

스칼라 타입 프로젝션

숫자,문자,날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다. 예를 들어 전체 회원의 이름을 조회하려면 다음처럼 쿼리한다.

List<String> usernames =

         em.createQuery("SELECT username FROM Member m",String.class).getResultList();

 

중복 데이터를 제거하려면 DISTINCT를 사용한다. 

SELECT DISTINCT username FROM Member m

 

여러 값 조회

엔티티를 대상으로 조회하면 편리하겠지만,꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다.

프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.

 

1
2
3
4
5
6
7
List resultList=em.createQuery.("SELECT m.username, m.age FROM Member m").getResultList();
 
for(Object[] row: resultList){
    String username=(String) row[0];
    Integer age=(Integer) row[1];
}
 
cs

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

1
2
3
4
5
6
7
List resultList=em.createQuery.("SELECT o.member,o.product,o.orderAmount FROM Order o").getResultList();
 
for(Object[] row: resultList){
    Member member=(Member)row[0];
    String username=(String) row[1];
    Integer age=(Integer) row[2];
}
cs

 

NEW 명령어 

username,age 두 필드를 프로젝션해서 타입을 지정할 수 없으므로 TypeQuery를 사용할 수 없다. 따라서 Object []를 반환 받았다. 실제 app 개발시에는 Object[] 를 직접 사용하지 않고 UserDTO처럼 의미 있는 객체로 변환해서 사용할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
List<Object []> resultList=
        em.createQuery("SELECT m.uesrname,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);
}
return userDTOs;
 
//UserDTO
public class UserDTO{
    private String username;
    private int age;
    public UserDTO(String username, int age){
        this.username=username;
        this.age=age;
    }
}
 
cs

이런 객체 변환 작업은 지루하다. 이번에는 NEW 명령어를 사용해보자.

1
2
3
4
5
 TypedQuery<UserDTO> query =
            em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username,m.age" +
                    "FROM Member m", UserDTO.class);
List<UserDTO> resultList=query.getResultList();
 
cs

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

 

NEW명령어 사용시 주의사항

  • 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
  • 순서와 타입이 일치하는 생성자가 필요하다.

10.2.4 페이징 API

페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이다. 더 큰 문제는 db마다 페이징을 처리하는 SQL문법이 다르다는 점이다. 

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

  • setFirstResult(int startPosition):조회 시작 위치(0부터 시작한다.)
  • setMaxResults(int maxResult):조회할 데이터 수
1
2
3
4
5
6
7
TypedQuery<Member> query=
        em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC",
                Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();
 
cs

FirstResult의 시작은 0이므로 11번째부터 시작해서 총20건의 데이터를 조회한다. 

따라서 11~30번 데이터를 조회한다.

 db마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 db방언 덕분이다.

 

10.2.5 집합과 정렬

집합은 집합함수와 함께 통계 정보를 구할 때 사용한다. 예를 들어 다음 코드는 순서대로 회원수,나이 합, 평균 나이, 최대 나이, 최소 나이를 조회한다.

1
2
3
4
5
6
7
select
    COUNT(m),
    SUM(m.age),
    AVG(m.age)
    MAX(m.age),
    MIN(m.age)
from Member m
cs

 

'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

엔티티 설계 주의점  (0) 2021.05.10
객체지향 쿼리 언어(2)  (0) 2021.05.08
8장 프록시와 연관관계 관리  (0) 2021.05.05
7장  (0) 2021.05.04
6장  (0) 2021.05.02

+ Recent posts