Spring Boot/JPA

[JPA] JPQL (Java Persistence Query Language)

728x90

 

 

 JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다. 이번 글에서는 다양한 객체지향 쿼리 중 JPQL에 대해 다룰 것이다.

 

 

JPQL이란?

 JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다. 문법은 SQL과 유사하며 간결하다. JPQL은 결국 SQL로 변환된다.

 또한 JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다는 특징이 있다. 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 데이터베이스를 변경할 수 있다.

 

 

 

기본 문법과 쿼리 API

 JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE문을 사용할 수 있다. 아래의 JPQL 문법을 보면 전체 구조는 SQL과 비슷한 것을 알 수 있다.

select_문 :: =
        select_절
        from_절
        [where_절]
        [groupby_절]
        [having_절]
        [orderby_절]

update_문 :: = update_절 [where_절]

delete_문 :: = delete_절 [where_절]

SELECT문은 다음과 같이 사용한다. SQL과 유사하지만 몇 가지 다른 점이 있다.

SELECT m
FROM Member AS m
WHERE m.username = 'leveloper'

 

 

1. 대소문자 구분

 엔티티와 속성은 대소문자를 구분한다. 예를 들어, Member, username은 대소문자를 구분해줘야 한다. 반면에 SELECT, FROM, WHERE 같은 JPQL 키워드는 대소문자를 구분하지 않아도 된다.

2. 엔티티 이름

 JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티명은 @Entity(name="abc")로 지정할 수 있다. 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다.

3. 별칭은 필수

 Member AS m을 보면 Member에 m이라는 별칭을 주었다. JPQL은 별칭을 필수로 사용해야 한다. AS를 생략해서 Member m처럼 사용해도 된다.

 

 

 

TypeQuery, Query

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

TypeQuery 사용

TypedQuery<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]);
}

 

 Query 객체는 SELECT 절의 조회 대상이 둘 이상이면 Object[]를 반환한다. 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 더 편리하다.

 

 

 

파라미터 바인딩

파라미터 바인딩에는 이름 기준 파라미터와 위치 기준 파라미터가 있다. 위치 기준 파라미터보단 이름 기준 파라미터가 더 명확하다.

1. 이름 기준 파라미터

 이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다. 이름 기준 파라미터는 앞에 : 를 사용한다.

String param = "leveloper";

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

List<Member> resultList = query.getResultList();

 

2. 위치 기준 파라미터

 위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다. 아래의 예시처럼 메소드 체인 방식으로 작성할 수도 있다.

String param = "leveloper";

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

 

 

 

 

프로젝션

 SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다. 프로젝션 대상에는 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.

1. 엔티티 프로젝션

 원하는 객체를 바로 조회하는 것이 컬럼을 하나하나 나열해서 조회해야 하는 SQL과는 차이가 있다. 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

String query = "SELECT m FROM Member m";

List<Member> memberList = em.createQuery(query, Member.class)
                            .getResultList();

 

2. 임베디드 타입 프로젝션

 JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

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

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

 

3. 스칼라 타입 프로젝션

 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다. 통계 쿼리도 주로 스칼라 타입으로 조회한다.

List<String> usernameList =
    em.createQuery("select username from Member m", String.class)
      .getResultList();

 

4. new 명령어

 엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다. 이럴 때는 UserDTO처럼 의미 있는 객체로 변환해서 사용한다.

public class UserDTO{
    
    private String username;
    private int age;
    
    public UserDTO(String username, int age){
        this.username = username;
        this.age = age;
    }
}


TypedQuery<UserDTO> query =
    em.createQuery("select new jpabook.jpql.UserDTO(m.username, m.age) from Member m", UserDTO.class);
    
List<UserDTO> resultList = query.getResultList();

 

 

 

페이징 API

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

  • setFirstResult (int startPosition) : 조회 시작 위치 (0부터 시작한다.)
  • setMaxResults (int maxResult) : 조회할 데이터 수

 아래의 예시에서는 FirstResult의 시작이 10이므로 11번째부터 시작해서 총 20건의 데이터를 조회한다. 따라서 11~30번 데이터를 조회한다. 

TypedQuery<Member> query =
    em.createQuery("select m from Member m order by m.username DESC", Member.class);
    
query.setFirstResult(10);
query.setMaxResults(20);

List<Member> resultList = query.getResultList();

 

데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. 데이터베이스마다 SQL이 다른 것은 물론이고 오라클과 SQLServer는 페이징 쿼리를 따로 공부해야 SQL을 작성할 수 있을 정도로 복잡하다. 페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.

 

 

 

JPQL 조인

 JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.

내부 조인

 JPQL 내부 조인은 SQL의 조인과 약간 다르다. JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다. 아래의 예시에서 m.team이 연관 필드인데 연관  필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 뜻한다.

String teamName = "teamA";

String query = "select m from Member m inner join m.team t where t.name = :teamName";

List<Member> memberList = em.createQuery(query, Member.class)
    .setParameter("teamName", teamName)
    .getResultList();

 

외부 조인

 외부 조인은 기능상 SQL의 외부 조인과 같다. OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.

SELECT m
FROM Member LEFT (OUTER) JOIN m.team t

 

세타 조인

 세타 조인은 전혀 관계없는 엔티티도 조인할 수 있다. WHERE 절을 사용해서 세타 조인을 할 수 있다. 아래의 쿼리는 전혀 관련없는 Member.username과 Team.name을 조인한다.

SELECT count(m)
FROM Member m, Team t
where m.username = t.name

 

 

 

 

페치 조인 (Fetch Join)

 페치 조인은 SQL에서 이야기하는 조인의 종류가 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 join fetch 명령어로 사용할 수 있다. 페치 조인은 N + 1 문제를 해결하는 데 주로 사용되는 방법이다. N + 1 문제에 대한 내용은 여기에서 확인하자. 

String query = "select m from Member m join fetch m.team";

List<Member> memberList = em.createQuery(query, Member.class).getResultList();

for (Member member : memberList) {
    System.out.println("username : " + member.getUsername());
    System.out.println("teamname : " + member.getTeam().getName());
}

 

 회원과 팀을 지연 로딩(LAZY)로 설정했다고 하면, 회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티이게 된다. 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.

 

 

이상으로 JPQL의 기본적인 내용을 알아보았다.

이번 포스팅에서 다룬 내용 외에도 서브 쿼리, 엔티티 그래프, 다형성 쿼리 등의 많은 내용들이 있다. 

더 많은 정보는 오라클 문서를 참고하도록 하자.

 

 

 

출처 : 자바 ORM 표준 JPA 프로그래밍 / 김영한 지음
728x90

'Spring Boot > JPA' 카테고리의 다른 글

[JPA] JPA 변경 감지와 병합(merge)  (0) 2020.01.30