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
(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 |
댓글