본문 바로가기

JPA

[JPA] 객체지향 쿼리

객체지향 쿼리란?

객체지향 쿼리란 데이터베이스 테이블을 대상으로 조회하는 SQL과 달리 엔티티 객체를 대상으로 조회하는 쿼리를 의미한다.

가장 중요한 객체지향 언어로 JPQL(Java Persistence Query language)이 있다.

 

JPQL이 가장 중요한 이유는 이 글에서 추가로 소개할 Criteria, QueryDSL 같은 기술들은 JPQL을 편하게 작성하도록 도와주는 빌더 클래스일 뿐 결국 JPQL에 뿌리를 두고 있기 때문에 JPQL을 제대로 이해하지 못하면 위에 언급한 기술도 사용하기 어렵기 때문이다.

JPQL

JPQL의 특징

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

JPQL 작성 시 유의사항

SELECT m FROM Member AS m where m.username = "Hello"
  • 엔티티(Member)와 속성(username)은 대소문자를 구분한다. 반면 SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entity(name="XXX")로 지정할 수 있으며 지정하지 않으면 클래스 명을 기본값으로 사용한다. Order 같이 클래스 명이 데이터베이스 예약어가 아닌 이상 클래스 명을 엔티티 명으로 사용하는 것을 추천
  • JPQL은 별칭을 필수로 사용해아 한다. ex) SELECT m.username FROM Member m

TypeQuery, Query

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

// TypeQuery 예시
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

// Query 예시
Query query = em.createQuery("SELECT m.username, m.age from Member m");

결과 조회 메소드

  • getResultList() : 결과를 컬렉션으로 반환한다. 만약 결과가 없으면 빈 컬렌션을 반환한다.
  • getSingleResult() : 결과가 정확히 하나일 때 사용한다. 결과가 없으면 NoResultException 예외가 발생하고 결과가 하나 이상이면 NonUniqueResultException 예외가 발생한다.

파라미터 바인딩

JPQL은 파라미터를 받을 수 있는데 이름 기준 파라미터와 위치 기준 파라미터 두 가지 방법으로 받을 수 있다.

 

1. 이름 기준 파라미터

이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법으로 파라미터 앞에 : 을 사용하고 setParameter() 메소드를 이용해 파라미터를 바인딩한다.

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

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

2. 위치 기준 파라미터

위치 기준 파라미터는 위치 정보를 기반으로 파라미터를 바인딩 하는 방법으로 ? 뒤에 위치 값을 주면 된다.

TypedQuery<Member> query = 
    em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class);

query.setParameter(1, "User1");
List<Member> resultList = query.getResultList();

프로젝션(projection)

SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라 한다.

 

1. 엔티티 프로젝션

SELECT m FROM Member m	    // 회원
SELECT m.team FROM Member m // 팀

두 SQL문 모두 회원과 회원과 연관된 팀 엔티티를 바로 조회했다. 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

 

2. 스칼라 타입 프로젝션

숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다. 이렇게 값으로 조회한 데이터는 영속성 컨텍스트에서 관리되지 않는다.

SELECT m.username, m.age FROM Member m

3. NEW 명령어

2번 예시 같이 여러 값으로 조회한 쿼리 문은 TypedQuery 객체로 생성할 수 없다. 하지만 UserDTO라는 데이터 객체를 이용하면 TypedQuery로 받을 수 있는데 이때 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있어 객체 변환 작업을 줄일 수 있다.

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

NEW 명령어 사용시 주의사항

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

페이징 API

데이터베이스마다 페이징 처리하는 SQL 문법이 다르다. 그렇기 때문에 JPA는 아래 두 가지 API를 이용해 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0 부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC",
    Member.class);
    
query.setFirstresult(10);    // 10번 인덱스 글부터 조회를 시작한다. (11번째 글)
query.setMaxResults(20);     // 10번 글부터 20개의 글을 조회해온다.
query.getResultList();

GROUP BY, HAVING

GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어주고 HAVING은 GROUP BY와 함께 사용되며 GROUP BY 로 그룹화한 통계 데이터를 기준으로 필터링 한다.

// 팀 이름을 기준으로 그룹화한 뒤 그룹별 평균나이가 10살 이상인 팀의 이름, 
// 회원 수, 나이 총합, 평균 나이, 최대값, 최소값을 구하는 JPQL 
SELECT t.name, 
       COUNT(m.age), 
       SUM(m.age), 
       AVG(m.age), 
       MAX(m.age), 
       MIN(m.age)
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10

정렬(ORDER BY)

ORDER BY는 조회한 결과를 정렬할 때 사용한다. 

  • ASC : 오름차순(기본값)
  • DESC : 내림차순
// 나이를 기준으로 내림차순하고 나이가 같으면 이름을 기준으로 오름차순 정렬하는 JPQL
SELECT m FROM Member m ORDER BY m.age DESC, m.username ASC

JPQL 조인

내부 조인

INNER 생략 가능

// 팀A에 소속된 회원을 조회하는 JPQL
String teamName = "팀A"
SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = :teamName

 

외부 조인

OUTER 생략 가능

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

 

세타 조인

WHERE절을 사용하여 전혀 관계가 없는 엔티티를 조인할 수 있다. 이를 세타 조인이라고 한다.

세타 조인은 내부 조인만 지원한다.

// 회원 이름이 팀 이름과 같은 회원의 수를 조회하는 JPQL
SELECT COUNT(m) FROM Member m, Team t WHERE m.username = t.name

 

페치 조인

페치 조인은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다. 그렇기 때문에 연관된 엔티티는 프록시 객체가 아닌 실제 객체가 만들어지고 실제 엔티티이므로 연관관계의 주인인 엔티티(Member)가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 객체(Team)를 조회할 수 있다.

SELECT m FROM Member m join fetch m.team

 

페치 조인과 일반 조인의 차이

일반 조인의 경우 연관된 엔티티를 조회하기 위해 쿼리를 한 번 더 실행하지만 페치 조인의 경우 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있다. 그렇기 때문에 최적화가 필요한 경우에 자주 쓰이게 되고 N + 1 문제가 발생했을때 페치 조인으로 해결할 수 있다.

 

페치 조인 특징

  • 별칭을 사용할 수 없다.
  • 일대다 조인의 경우 조회 결과가 증가될 수 있다. => DISTINCT 명령어로 중복을 제거할 수 있다.
  • 글로벌 로딩 전략보다 우선한다. => 글로벌 로딩 전략이 LAZY로 설정되어 있어도 페치 조인을 적용한 JPQL은 연관된 엔티티까지 한번에 조회해온다.

서브 쿼리

서브쿼리는 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.

 

서브 쿼리 예시

// 나이가 평균보다 많은 회원을 조회하는 JPQL
SELECT m FROM Member m WHERE m.age > (SELECT avg(m2.age) FROM Member m2)

// 한 건이라도 주문한 고객을 조회하는 JPQL
SELECT m FROM Member m WHERE (SELECT count(o) FROM Order o WHERE m = o.member) > 0

서브 쿼리 함수

  • EXISTS : 서브쿼리에 결과가 존재하면 참 <=> NOT EXISTS
  • ALL : 조건을 모두 만족하면 참
  • ANY, SOME : 조건을 하나라도 만족하면 참
  • IN : 서브쿼리 결과 중 하나라도 같은 것이 있으면 참

CASE 식

기본 CASE

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

 

심플 CASE

심플 CASE는 조건식을 사용할 수 없다.

SELECT
    CASE t.name
         WHEN "팀A" THEN "인센티브110%"
         WHEN "팀B" THEN "인센티브120%"
         ELSE "인센티브105%"
    END
FROM Team t

 

COALESCE

스칼라 식을 차례대로 조회해서 null이 아니면 반환한다.

// null이 아니면 회원 이름, null이면 "이름 없는 회원"이 반환되는 JPQL
SELECT COALESCE(m.username, "이름 없는 회원") FROM Member m

 

NULLIf

두 값이 같으면 null을 반환하고 다르면 첫 번째 값을 반환한다.

집합 함수는 null을 포함하지 않으므로 보통 집합 함수와 함께 사용한다.

// m.username이 관리자이면 null을 반환하고 아니면 m.username을 반환하는 JPQL
SELECT NULLIF(m.username, "관리자") from Member m

Named 쿼리 : 정적 쿼리

정적 쿼리 : 미리 정의한 쿼리에 이름을 부여하여 필요할 때 사용할 수 있도록 하는 방법, 이를 Named 쿼리라고 한다.

동적 쿼리 : em.createQuery()처럼 JPQL을 문자로 완성해서 직접 넘기는 방법

 

정적 쿼리 사용 시 장점

  • 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 두기 때문에 오류를 빠르게 확인할 수 있고 재사용이 가능하기 때문에 성능상 이점이 있다.
  • 변하지 않는 정적인 SQL을 생성하기 때문에 데이터베이스의 조회 성능 최적화에 도움이 된다.

사용방법

@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member {

    ...
    
    private String username;
    
    ...
}

Criteria

JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API

Criteria 장점

  • 컴파일 시점에 오류를 발견할 수 있다.
  • IDE를 사용하면 코드 자동완성을 지원한다.
  • 동적 쿼리를 작성하기 편하다.

Criteria 단점

  • 위 장점을 모두 상쇄할 정도로 복잡하고 장황하다. 

Criteria 보다는 같은 빌더 클래스이면서 보다 사용하기 쉬운 QueryDSL을 사용하는 것을 추천한다.

QueryDSL

QueryDSL은 Criteria 처럼 빌더 클래스이다.

Criteria가 가지고 있는 장점을 갖고 있으면서 단순하고 사용하기 쉽다.