본문 바로가기
  • 실행력이 모든걸 결정한다
Spring 사전 준비/JPA Hibernate

[JPA] JPQL의 작성

by 김코더 김주역 2022. 8. 4.
반응형

1. JPQL 소개

- JPQL(Java Persistence Query Language)은 객체지향 SQL이다.

- SQL이 DB 테이블을 대상으로 하는 데이터 중심의 쿼리라면, JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다. JPA는 JPQL을 분석해서 적절한 SQL을 만들어서 DB를 조회하고, 그 결과로 엔티티 객체를 반환한다.

- JPQL은 SQL을 추상화해서 특정 DB에 의존하지 않는다. 심지어, SQL 함수도 표준화되어 있다.

- JPQL은 엔티티 직접 조회, 묵시적 조인, 다형성을 지원하기 때문에 SQL보다 간결하다.

- JPA가 JPQL을 분석해서 SQL을 생성할 때는 즉시 로딩과 지연 로딩을 구분하지 않고 JPQL 자체에만 충실하다.

- Criteria, QueryDSL은 JPQL을 편하게 작성하도록 도와주는 빌더 클래스의 모음이다. 이들은 쿼리 전용 클래스를 이용하여 쿼리를 프로그래밍 코드로 작성하기 때문에 컴파일 시점에 오류를 발견할 수 있고, IDE 상에서 코드 자동완성 기능을 이용할 수 있다.

 

 

 

2. JPQL 문법

1) 기본 문법

(1) SELECT

- 명시 순서 : SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY

- 연산 순서 : FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY

- 엔티티와 속성의 이름은 대소문자를 구분한다.

- 별칭은 필수로 줘야 한다.

SELECT m FROM Member m;  #Member의 별칭으로 m을 사용함

※ Hibernate에서 제공하는 HQL이라는 언어를 사용하면 별칭 없이 사용할 수 있다.

 

(2) TypeQuery, Query

- 작성한 JPQL을 실행하기 위한 쿼리 객체다.

- 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery를, 그렇지 않으면 Query를 사용하면 된다.

※ 여러 엔티티나 컬럼을 선택할 때는 반환할 타입이 명확하지 않을 수가 있기 때문에 Query를 사용한다.

// TypedQuery
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();

// Query
Query query = em.createQuery("SELECT m.name, m.age, m.team FROM Member m");
List<Object[]> resultList = query.getResultList();
for(Object[] row : resultList) {
    String name = (String) row[0];
    Integer age = (Integer) row[1];
    Team team = (Team) row[2];
}

- setFirstResult(), setMaxResults() 메소드로 페이징을 처리할 수도 있다.

query.setFirstResult(20); // 21번째 데이터부터 조회
query.setMaxResults(10); // 데이터 10개 조회
query.getResultList(); // 결과적으로 21~30번째 데이터를 조회하게 됨

- getResultList() 말고도 결과가 정확히 하나일 때 사용하는 getSingleResult() 메소드가 있다. 결과가 1개가 아니면 예외가 발생한다. (NoResultException, NonUniqueResultException)

 

 

2) 파라미터 바인딩

(1) 이름 기반 파라미터

- 변수 앞에 :를 붙인다.

String jpql = "select m from Member m join m.team t where t.name=:teamName"; // 앞에 :가 붙은 변수에 파라미터를 바인딩 받는다.
List<Member> memberList = em.createQuery(jpql, Member.class)
    .setParameter("teamName", "group"); // :teamName에 파라미터 바인딩
    .getResultList();

- 추천하는 방식이다.

 

(2) 위치 기반 파라미터

- ?뒤에 위치 값을 붙인다.

String jpql = "select m from Member m join m.team t where t.name=?1";
List<Member> memberList = em.createQuery(jpql, Member.class)
    .setParameter(1, "group");
    .getResultList();

 

 

3) 조회 대상 분류

(1) 엔티티

- 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

 

(2) 임베디드 타입

- 임베디드 타입은 조회의 시작점이 될 수 없다.

- 엔티티가 아닌 값 타입이기 때문에 영속성 컨텍스트에서 관리되지 않는다.

 

(3) 스칼라 타입

- 기본 데이터 타입인 특정 컬럼의 모든 값들을 조회하는 것이다.

List<String> usernames = em.createQuery("SELECT DISTINCT name FROM Member m", String.class).getResultList();

- COUNT, SUM, AVG, MIN, MAX 등의 통계 쿼리를 조회할 때 유용하다.

 

 

4) NEW

- JPQL 조회 결과를 반환받을 클래스의 생성자에 곧바로 전달해서 엔티티를 반환받는 방법이다. 아래 예시와 같이 클래스는 패키지명을 포함해서 작성해야 한다.

TypedQuery<UserDTO> query = em.createQuery("SELECT new com.example.demo.dto.UserDTO(m.name, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();

 

 

5) 통계

(1) 집합 함수

<1> 집합 함수의 종류

- COUNT : 결과 수를 Long 타입으로 반환

- MAX, MIN : 최댓값과 최솟값

- AVG : 평균값을 Double 타입으로 반환

- SUM : 사용하는 타입에 맞게 합계를 반환

<2> 주의점

- 집합 함수는 NULL값을 무시하고 계산한다.

- 집합 함수 안에 DISTINCT를 사용할 수 있다.

- 값이 없을 때 집합 함수를 사용하면 NULL이 반환된다. (COUNT 제외)

- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.

예시

SELECT COUNT(m), AVG(m.age), MAX(m.age) from Member m;

 

(2) 그룹화

- GROUP BY : 특정 그룹끼리 묶어주는 키워드

- HAVING : GROUP BY로 그룹화된 데이터를 기준으로 조건을 거는 키워드

예시

// SELECT ~ FROM ~ WHERE ~
GROUP BY t.name
HAVING AVG(m.age) >= 19

 

(3) 정렬

- ORDER BY : 결과를 정렬하는 키워드로, 오름차순인 ASC(기본값) 키워드와 내림차순인 DESC 키워드를 같이 사용할 수 있다.

예시

// SELECT ~ FROM ~ WHERE ~ GROUP BY ~ HAVING ~
ORDER BY m.age DESC, m.name ASC

 

 

6) JOIN

- JOIN은 조회의 주체가 되는 엔티티만 조회하고 영속화하기 때문에 JOIN은 주로 연관 엔티티가 검색조건에 필요한 경우에 사용된다. 연관 엔티티를 조회해야 한다면 별도의 쿼리문을 실행해야 하기 때문에 N+1 문제가 발생할 수 있다. JOIN으로 N+1 문제를 해결하려면 4)에서 살펴본 방법대로 DTO 변환을 이용하면 된다.

- 연관 엔티티까지 함께 영속화 하려면, 잠시 후에 살펴볼 FETCH JOIN을 고려하도록 하자.

 

(1) 내부 조인

- INNER JOIN 또는 JOIN 키워드를 이용한다.

- 반드시 연관 필드를 사용하여 조인해야 한다.

예시

- 회원과 회원이 속한 팀을 내부 조인하는 예시다. 

SELECT m FROM Member m JOIN m.team t WHERE t.name=:teamName

 

(2) 외부 조인

- OUTER 키워드는 생략 가능해서 보통 LEFT JOIN으로 사용한다.

예시

SELECT m FROM Member m LEFT JOIN m.team t

 

(3) 컬렉션 조인

- 컬렉션 연관 필드를 사용하여 조인한다.

예시

SELECT t, m FROM Team t LEFT JOIN t.members m

 

(4) 세타 조인

- 내부 조인만 지원한다.

- SQL의 CROSS JOIN으로 변환된다.

예시

SELECT COUNT(m) from Member m, Team t WHERE m.name = t.name

 

(5) JOIN ON

- 내부 조인의 ON 절은 WHERE 절을 사용했을 때와 결과가 같기 때문에 보통 ON 절은 외부 조인에서만 사용한다.

- 조인 시점에 조인 대상을 필터링한다.

예시

SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name='TeamA'

 

 

7) JOIN FETCH

- 연관된 엔티티나 컬렉션을 SQL 한 번에 같이 조회하여 영속화한다. 따라서, 성능 최적화에 도움이 된다.

※ FetchType.LAZY 설정보다 우선순위가 높다.

- 현재 엔티티가 준영속 상태가 되어도 연관 엔티티는 여전히 영속성 컨텍스트에서 조회할 수 있기 때문에 객체 그래프 탐색이 가능하다.

- 패치 조인 대상에 별칭은 사용할 수 없지만, 하이버네이트에서는 사용 가능하다.

 

(1) 엔티티 패치 조인

- 내부적으로 INNER JOIN이 이루어져서 연관 엔티티까지 한 번에 조회된다. (SELECT M.*, T.*)

예시

SELECT m FROM Member m join fetch m.team

 

(2) 컬렉션 패치 조인

- 내부적으로 INNER JOIN이 이루어져서 연관 엔티티까지 한 번에 조회된다. (SELECT T.*, M.*)

- 일대다 관계이기 때문에 결과 수가 증가한다.

예시

SELECT t FROM Team t JOIN FETCH t.members WHERE t.name='TeamA'

※ DISTINCT를 사용하지 않았기 때문에 동일한 Team 엔티티들이 중복으로 조회된다. 자세한 내용은 [(3) DISTINCT 사용]을 참고하자.

SQL JOIN 결과

[TEAM {ID, NAME}, MEMBER{ID, TEAM_ID, NAME}]

1 TeamA 1 1 member1
1 TeamA 2 1 member2

 

(3) DISTINCT 사용

- JPQL에 DISTINCT를 사용하면 SQL과 애플리케이션 두 곳에서 모두 중복을 제거한다. SQL에서는 JOIN 결과를 바탕으로 중복을 제거하고 애플리케이션에서는 현재 엔티티의 중복을 제거한다.

예시

SELECT DISTINCT t FROM Team t JOIN FETCH t.members WHERE t.name='TeamA'

 

(4) FETCH JOIN 전략과 한계

- 글로벌 로딩 전략은 기본적으로 지연 로딩을 사용하고 최적화가 필요하면 패치 조인을 사용하는 것이 좋다.

- 둘 이상의 컬렉션을 패치할 수 없다.

- 컬렉션을 패치 조인하면 setFirstResult(), setMaxResult()와 같은 페이징 API를 사용했을 때 성능 문제와 메모리 초과 예외가 발생할 수 있다.

- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 4)에서 살펴본 방법대로 DTO 변환을 이용하는 것이 더 효과적일 수 있다.

 

 

8) 경로 표현식

- .(점)을 통해 객체 그래프를 탐색하는 것을 말한다.

 

(1) 경로의 종류와 특징

- 상태 필드 경로 : 계속 탐색할 수 없다.

- 단일 값 연관 경로 : 계속 탐색할 수 있으며, 묵시적으로 내부 조인이 일어난다.

- 컬렉션 값 연관 경로 : 계속 탐색할 수 없으며, 묵시적으로 내부 조인이 일어난다. 경로 탐색을 하고 싶다면 아래와 같이 명시적으로 조인을 사용해서 새로운 별칭을 획득하면 된다.

SELECT m.name FROM Team t JOIN t.members m

 

(2) 묵시적 조인

- 경로 표현식에 의해 묵시적으로 내부 조인이 일어나는 것

- 연관 엔티티의 식별자에 접근하는 경우에는 조인이 발생하지 않는다. 테이블에 이미 외래 키로 저장되어 있기 때문이다.

- 연관 엔티티의 임베디드 타입에 접근하는 경우에도 조인이 발생하지 않는다.

예시

- Member에 Team team 연관 필드가 존재한다고 하면 다음과 같이 간단하게 묵시적 조인이 가능하다.

SELECT m.team FROM Member m

 

(3) 컬렉션의 크기

- 컬렉션에 .size를 붙이면 COUNT 함수를 사용하는 SQL로 변환된다.

SELECT t.members.size FROM Team t

 

 

9) 서브 쿼리

(1) 서브 쿼리 소개

- 서브 쿼리는 SELECT문 안에 존재하는 또 다른 SELECT문을 의미한다.

- 보통 서브 쿼리는 WHERE, HAVING 절에서만 사용할 수 있다.

※ Hibernate에서 제공하는 HQL이라는 언어는 SELECT 절의 서브 쿼리도 허용한다.

# 평균 나이 이하인 회원들을 조회
SELECT m FROM Member m WHERE m.age <= (SELECT AVG(m2.age) from Member m2)

 

(2) 서브 쿼리 함수

- EXISTS : 서브 쿼리에 결과가 존재하면 참

# 팀 이름이 'Team'으로 시작하는 팀에 속한 회원들을 조회
SELECT m FROM Member m WHERE EXISTS (SELECT t FROM m.team t WHERE t.name LIKE 'Team%')

- ALL : 조건을 모두 만족하면 참

# 모든 제품의 각 재고량이 주문량보다 적은 주문들을 조회
SELECT o FROM Order o WHERE o.orderAmount > ALL(SELECT p.amount FROM Product p)

- ANY, SOME : 조건을 하나라도 만족하면 참

# team이 존재하는 회원들을 조회
SELECT m FROM Member m WHERE m.team = ANY(SELECT t FROM Team t)

- IN : 서브쿼리의 결과 중 하나라도 일치하는 것이 있으면 참

 

 

10) 조건식

(1) 타입 표현

- 대소문자는 구분하지 않는다.

- 문자 : 작은 따옴표 사이에 표현한다. 작은 따옴표를 표시하려면 작은 따옴표를 연속으로 두 번('') 사용하면 된다.

- 숫자 : 숫자 뒤에 L(Long 타입), D(Double 타입), F(Float 타입)를 붙여서 타입을 지정한다.

5L
3.5D
4.1F

- 날짜

DATE {d 'yyyy-mm-dd'}
TIME {t 'hh-mm-ss'}
DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'}
o.createDate = {d '2022-08-09'}

- Boolean : TRUE/FALSE

- Enum : 패키지명을 포함한 전체 이름을 사용해야 한다.

- 엔티티 타입 : 주로 상속과 관련해서 사용한다.

TYPE(m) = Member

 

(2) 연산자 우선 순위

- 경로 탐색 연산(.) > 수학 연산 > 비교 연산(LIKE, IN, EXISTS 등 포함) > 논리 연산

 

(3) 논리 연산, 비교식

- SQL과 다르지 않기 때문에 설명은 생략하겠다.

 

(4) 컬렉션 식

- 컬렉션에만 사용하는 특별한 식이다.

- 빈 컬렉션 비교 식 : IS [NOT] EMPTY

# SELECT ~ FROM ~
WHERE m.orders IS NOT EMPTY  # 컬렉션에 IS NULL은 사용할 수 없다.

 

- 컬렉션의 멤버 식 : {엔티티나 값} [NOT] MEMBER [OF] {컬렉션}

# 파라미터로 넣은 Member 엔티티가 t.members 컬렉션에 포함되어 있는지 비교
SELECT t FROM Team t WHERE :memberParam MEMBER OF t.members

 

(5) 스칼라 식

- 수학 식 : *, /, +, -

- 문자 함수 : CONCAT(), SUBSTRING(), TRIM(), LOWER(), UPPER(), LOCATE()

- 수학 함수 : ABS(), SQRT(), MOD(), SIZE(), INDEX()

- 현재 날짜 정보 : CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP

※ Hibernate는 날짜 타입 값으로부터 년, 월, 일, 시간, 분, 초를 구할 수 있는 YEAR(), MONTH(), DAY(), HOUR(), MINUTE(), SECOND() 함수를 지원한다. 예를 들어, YEAR(CURRENT_TIMESTAMP) 처럼 사용할 수 있다.

 

(6) CASE 식

- 특정 조건에 따라 분기할 때 사용된다.

- 기본 CASE

SELECT
    CASE
        WHEN m.age <= 13 THEN '학생요금'
        WHEN m.age >= 65 THEN '경로요금'
        ELSE '일반요금'
    END
FROM Member m

- 심플 CASE : 프로그래밍 언어의 SWITCH문과 유사하게 동작함

SELECT
    CASE t.name
        WHEN 'TeamA' THEN '인센티브120%'
        WHEN 'TeamB' THEN '인센티브110%'
        ELSE '인센티브100%'
    END
FROM Team t

- COALESCE : 값이 NULL인 경우에 NULL 대신에 반환할 값을 지정하며, NULL이 아닌 경우에는 첫 번째 값을 반환함.

SELECT COALESCE(m.name, '이름 없음') FROM Member m

- NULLIF : 지정한 값 대신에 NULL을 반환시키도록 하며, NULL을 반환하지 않는 경우에는 첫 번째 값을 반환함.

SELECT NULLIF(m.name, '관리자') FROM Member m

 

 

11) 다형성 쿼리

- 객체의 상속 관계 매핑은 아래 포스팅의 [1. 객체의 상속 관계 매핑]을 참고하자.

https://kimcoder.tistory.com/486

 

[JPA] 고급 매핑 기술

1. 객체의 상속 관계 매핑 - 객체의 상속 구조와 DB의 슈퍼타입-서브타입 관계를 매핑하는 것이다. - 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회된다. 1) 조인 전략 (1) 특징 - 엔티티들을

kimcoder.tistory.com

 

(1) TYPE

- 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 사용한다.

# Item 중에서 FRUIT, BOOK 타입만 조회한다.
SELECT i FROM Item i WHERE TYPE(i) IN (FRUIT, BOOK)

 

(2) TREAT

- 엔티티의 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. JPA 2.1에 추가된 기능이다.

- 자바의 타입 캐스팅과 비슷하다.

- 보통 TREAT는 FROM, WHERE 문에서 사용할 수 있지만, Hibernate에서는 SELECT절에도 사용 가능하다.

# 부모 타입인 Item을 자식 타입인 Book으로 다룬다.
SELECT i FROM Item i WHERE TREAT(i as Book).author = "Kimcoder"

 

 

12) 사용자 정의 함수 호출

- 특정 DB에 저장되어 있는 사용자 정의 함수를 호출할 수 있다. JPA 2.1에 추가된 기능이다.

- FUNCTION 함수의 첫 번째 인자에는 함수명이 들어가고 두 번째 인자부터는 함수 파라미터가 들어간다.

SELECT FUNCTION('create_prefix', m.name) from Member m

※ Hibernate 사용 시 "SELECT create_prefix(m.name) From Member m"로 축약해서 작성할 수도 있다.

- 이 기능을 사용하려면 방언 클래스를 상속하고, 필요한 사용자 지정 함수들을 미리 등록해놓아야 한다.

public class MyH2Dialect extends H2Dialect {
    
    public MyH2Dialect() {
        registerFunction("create_prefix", // DB에 등록된 사용자 지정 함수 이름
            new StandardSQLFunction("create_prefix", StandardBasicTypes.STRING); // 사용자 정의 함수 별칭, 반환 타입
    }
}

그리고 방언 설정을 이 방언 클래스로 지정하면 된다.

<property name="hibernate.dialect" value="com.example.demo.dialect.MyH2Dialect" />

 

 

13) 헷갈릴 수 있는 비교 연산

- enum은 = 비교 연산만 지원한다.

- 임베디드 타입은 비교를 지원하지 않는다.

- JPA 표준은 길이가 0인 빈 문자열도 String 타입으로 정하지만 빈 문자열을 NULL로 사용하는 데이터베이스도 있기 때문에 비교 연산시 주의해야 한다.

- NULL과의 모든 수학적 계산 결과는 NULL이다.

- NULL == NULL은 알 수 없는 값이다. NULL을 비교하려면 IS [NOT] NULL을 사용해야 한다.

 

 

14) 엔티티 직접 사용

- 엔티티를 직접 사용하면 JPQL이 SQL으로 변환될 때 해당 엔티티의 기본 키를 사용한다.

※ 여기에서 "사용"한다는 것은 컬럼값 조회를 말하는게 아니니 오해하지 말자.

 

 

15) Named 쿼리

- 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는 정적 쿼리를 의미한다.

- 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 파싱해두기 때문에 오류를 빨리 확인할 수 있고 성능상 이점이 있다.

- Named 쿼리는 @NamedQuery 어노테이션 또는 XML 문서에 정의할 수 있다.

 

(1) @NamedQuery 어노테이션에 정의

- @NamedQuery의 name 속성에 쿼리 이름을 부여하고 query 속성에 쿼리를 입력한다.

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

※ @NamedQuery.lockMode : 락 모드 설정 [기본값 NONE]

※ @NamedQuery.hints : JPA 구현체에게 제공하는 힌트로, 2차 캐시를 다룰 때 사용함

- Named 쿼리를 사용할 때는 EntityManager의 createNamedQuery() 메소드의 파라미터에 Named 쿼리 이름을 넣어주면 된다.

List<Member> resultList = em.createNamedQuery("Member.findByName", Member.class)
    .setParameter("username", "jooyeok")
    .getResultList();

 - 하나의 엔티티에 여러 개의 Named 쿼리를 정의하려면 @NamedQueries 어노테이션을 사용하면 된다.

@Entity
@NamedQueries(
    @NamedQuery(...),
    @NamedQuery(...)
)
public class Member {...}

 

(2) XML에 정의

- Named 쿼리를 작성하는 것은 XML을 사용하는 것이 편리하다. SQL(JPQL)은 멀티라인으로 작성해두는 것이 가독성에 좋은데, 자바 언어로 멀티라인 문자를 다루는 것은 복잡하기 때문이다.

- JPA가 기본적으로 인식하는 매핑파일은 META-INF/orm.xml인데, 매핑 파일을 직접 추가하는 방법도 알아볼 겸 이름을 다르게 지정해보았다.

<META-INF/ormMember.xml>

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="htt://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
    <named-query name="Member.findByName">
        <query>
            <![CDATA[SELECT m FROM Member m WHERE m.username = :username]]>
        </query>
    </named-query>
</entity-mappings>

※ XML에서 &, <, >는 예약 문자다. 그래서 만일에 대비해서 쿼리문은 <!CDATA[ ]]>를 이용하여 문장을 그대로 출력하는 것이 안전하다.

- 그리고 위에 생성한 매핑 파일을 인식하도록 META-INF/persistence.xml에 아래와 같이 설정을 추가했다.

<persistence-unit name="hello">
    <mapping-file>META-INF/ormMember.xml</mapping-file>
    ...
>

 

(3) 설정의 우선 순위

- 어노테이션과 XML에 같은 설정이 있으면 XML이 우선권을 가진다.

 

 

 

● 참고자료 : 자바 ORM 표준 JPA 프로그래밍

반응형

'Spring 사전 준비 > JPA Hibernate' 카테고리의 다른 글

[JPA] 네이티브 SQL  (0) 2022.08.11
[JPA] QueryDSL  (0) 2022.08.10
[JPA] 값 타입 컬렉션  (0) 2022.08.03
[JPA] 복합 값 타입과 불변 객체  (0) 2022.08.03
[JPA] CASCADE 기능  (0) 2022.08.03

댓글