10.2 JPQL
어떤 방법을 사용하든 JPQL(Java Persistence Query Language)에서 모든 것이 시작.
- JPQL은 객체지향 쿼리 언어이다. 테이블 대상이 아닌 엔티티 객체를 대상.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
10.2.1 기본 문법과 쿼리 API
SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
INSERT 문은 없다. -> EntityManager.persist() 메소드 사용.
JPQL 문법
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
SELECT 문
SELECT m FROM Member AS m where m.username = 'Hello'
대소문자 구분
- 엔티티와 속성은 대소문자를 구분한다.
- SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
엔티티 이름
- JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티명이다.
- 엔티티명은 @Entity(name="xxx")로 지정할 수 있다.
- 지정하지 않으면 클래스명이 기본값.
- 기본값인 클래스명을 엔티티명으로 사용하는 것을 추천
별칭은 필수
- JPQL은 별칭을 필수로 사용해야 한다.
- 'AS'는 생략 가능.
//잘못된 문법
SELECT username FROM Member m // username -> m.username으로 고쳐야 함.
TypeQuery, Query
JPQL을 실행하려면 쿼리 객체를 만들어야 한다.
쿼리 객체
- TypeQuery
- 반환할 타입이 명확한 경우
- Query
- 반환 타입이 명확하지 않은 경우
- Select 절의 조회 대상이 둘 이상이면 Object[] 반환
- Select 절의 조회 대상이 하나면 Object를 반환
TypeQuery 사용
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 사용
결과가 1개
String jpql1 = "select m.username from Member m";
Query result = em.createQuery(jpql1);
List resultList = result.getResultList();
for (Object object : resultList) {
String username = (String) object;
System.out.println("1 = " + username);
}
결과가 2개 이상
String jpql1 = "select m.username, m.age from Member m;
Query result = em.createQuery(jpql1);
List resultList = result.getResultList();
for (Object object : resultList) {
Object[] objects = (Object[]) object;
System.out.println("1 = " + objects[0]);
System.out.println("2 = " + objects[1]);
}
타입 변환이 필요없는 TypeQuery 사용하는 것이 더 편리
결과 조회
- query.getResultList()
- 결과가 없으면 빈 컬렉션 반환
- query.getSingleResult()
- 결과가 정확히 하나일 때 사용.
- 결과가 없으면 예외 발생 : NoResultException
- 1개보다 많으면 : NonUniqueResultException
- 주의 필요.
10.2.2 파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원.
JPQL은 이름 기준 파라미터 바인딩도 지원
이름 기준 파라미터
앞이 ":"을 사용한다.
이름 기준 파라미터 사용
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();
이름 기준 파라미터 메소드 체인 방식
String usernameParam = "User1";
List<Member> members =
em.createQuery(""SELECT m FROM Member m where m.username = :username,
Member.class)
.setParameter("username", usernameParam)
.getResultList();
위치 기준 파라미터
?
다음에 위치값을 주면 된다. 위치 값은 1부터 시작.
위치 기준 파라미터
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = ?1",
Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확한다.
10.2.3 프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(Projection)이라 한다.
프로젝션 대상
엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터)
엔티티 프로젝션
조회한 엔티티는 영속성 컨텍스트에서 관리된다.
SELECT m FROM Member m // 멤버
SELECT m.team FROM Member.m // 팀
임베디드 타입 프로젝션
엔티티와 거의 비슷. 조회의 시작점이 될 수 없다.
// 잘못된 쿼리
String query = "SELECT a FROM Address a";
String query = "SELECT o.address FROM Order o";
List<Address> address = 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
통계 쿼리 가능
Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o",
Double.class)
.getSingleResult();
여러 값 조회
프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신 Query를 사용해야 한다.
Query query =
em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
제너릭으로 좀 더 편하게
조회한 엔티티는 영속성 컨텍스트에서 관리된다.
List<Object[]> resultList =
em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
.getResultList();
for (Object[] row : resultList) {
Member member = (Member) row[0]; //엔티티
Product product = (Product) row[1]; //엔티티
int orderAmount = (Integer) row[2]; //스칼라
}
NEW 명령어
실제 개발에서 Object[]를 직접 사용하지 않고 DTO 형태의 의미있는 객체로 변환해서 사용.
샘플 UserDTO
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
// ...
}
NEW 명령어 사용
NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있다.
TypeQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
NEW 명령어 주의 사항
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요한다.
10.2.4 페이징 API
페이징 처리용 SQL은 지루하고 반복적.
JPA는 페이징을 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
페이징 사용
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC",
Member.class);
// 11번째부터 20건의 데이터 조회, 11~30
query.setFirstResult(10);
query.setMaxResult(20);
query.getResultList();
10.2.5 집합과 정렬
집합은 집합함수와 함께 통계 정보를 구할 때 사용.
select
COUNT(m), // 회원수
SUM(m.age), // 나이 합
AVG(m.age), // 나이 평균
MAX(m.age), // 최대 나이
MIN(m.age), // 최소 나이
from Member m
집합 함수 사용시 주의 사항
- NULL 값은 무시, 통계에 잡히지 않는다.
- 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단, COUNT는 0.
- DISTINCT를 집합 함수에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
select COUNT( DISTINCT m.age) from Member m
- DISTINCT를 COUNT에 사용시 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
- GROUP BY - 특정 그룹끼리 묶어준다.
- HAVING - GROUP BY와 함께 사용, GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링.
예제코드
// 평균 나이가 10살 이상인 그룹을 조회
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)
결과를 정렬할 때 사용한다.
select m from Member m order by m.age DESC, m.username ASC
- ASC : 오름차순(기본값)
- DESC : 내림차순
10.2.6 JPQL 조인
SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
내부 조인은 INNER JOIN을 사용. INNER는 생략 가능.
연관 필드를 사용하여 조인.
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
잘못된 경우
FROM Member m JOIN Team t // 오류!
외부 조인
외부조인 JPQL
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
OUTER는 생략 가능. 보통 LEFT JOIN으로 사용.
컬렉션 조인
일대다 관계, 다대다 관계처럼 컬렉션을 사용하는 곳에 조인.
- [회원 ->팀]으로의 조인은 다대일 조인, 단일 값 연관 필드(m.team) 사용.
- [팀 -> 회원]은 반대로 일대다 조인, 컬렉션 값 연관 필드(m.members) 사용.
세타 조인
세타 조인은 내부 조인만 지원한다.
전혀 관계없는 엔티티도 조회할 수 있다.
전혀 관계없는 Member.username과 Team.name을 조인
//JPQL
select count(m) from Member m, Team t
where m.username = t.name
//SQL
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WHERE
M.USERNAME = T.NAME
JOIN ON절(JPA 2.1)
JPA 2.1부터 조인할 때 ON 절을 지원. 조인 대상을 필터링하고 조인할 수 있다.
내부 조인의 ON 절은 WHERE절과 결과가 같음.
보통 ON절은 외부 조인에 사용.
//JPQL
select m, t from Member m
left join m.team t on t.name = 'A'
//SQL
SELECT m.*, t.*
FROM Member m
LEFT JOIN Team t ON m.team_id = t.id and t.name = 'A'
10.2.7 페치 조인
페치 조인은 SQL의 조인 종류가 아님.
JPQL에서 성능 최적화를 위해 제공하는 기능
페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회
일반적인 JPQL 조인과 다르게 별칭 없음.
페치 조인은 별칭을 사용할 수 없다.
select m
from Member m join fetch m.team
실행된 SQL
SELECT
M.*, T.*
FROM MEMBER T
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
JPQL을 사용하는 페치 조인
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
//페치조인으로 회원과 팀을 함께 조회 -> 지연로딩 발생 안 함.
System.out.println("username = " + memrber.getUserName() + ", " +
"teamname = " + member.getTeam().name());
...
// 출력결과
username = 회원 1, teamname = 팀A
username = 회원 2, teamname = 팀A
username = 회원 3, teamname = 팀B
}
회원과 팀을 지연 로딩 설정
- 회원 조회시 페치 조인을 사용해서 팀을 함께 조회.
- 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티.
- 지연 로딩이 일어나지 않는다.
- 실제 엔티티이므로 회원 엔티티가 준영속 상태가 되어도 팀 조회 가능.
컬렉션 페치 조인
페치 조인의 특징과 한계
- 별칭을 줄 수 없다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 페이징 API를 사용할 수 없다.
10.2.8 경로 표현식
경로 표현식이란 .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username
from Member m
join m.team t
join m.orders o
where t.name = '팀A'
m.username, m.team, m.orders, t.name 모두 경로 표현식
많이 하는 실수, 컬렉션 값에서 경로 탐색 시도
select t.members from Team t //성공
select t.members.username from Team t //실패
컬렉션에서 경로 탐색하려면
// join t.members m으로 컬렉션에 새로운 별칭을 얻음.
select m.username from Team t join t.members m
SIZE라는 특별한 기능 사용
select t.members.size from Team t
서브쿼리
SQL처럼 서브 쿼리를 지원한다.
나이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
한 건이라도 주문한 고객
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
10.2.10 조건식
타입표현
종류 | 예제 |
---|---|
문자 | 'HELLO', 'Hi' |
숫자 | 10L, 10D, 10F |
날짜 | {d'2012-03-24'} |
타임 | {t'10-11-11'} |
DATETIME | [ts'2014-03-24 10-11-11.123'} |
Enum | jpabook.MemberType.Admin |
엔티티 타입 | TYPE(m) = Member |
Between, IN, Like, Null
Between
select m from Member m
where m.age between 10 and 20
IN 식
select m from Member m
where m.username in ('회원1', '회원2')
Like 식
// 중간에 '원'이 들어간 회원
select m from Member m
where m.username like '%원%'
where m.username like '회원%'
where m.username like '%회원'
//회원A, 회원1
where m.username like '회원_'
//회원3
where m.username like '__3'
NULL 비교식
where m.username is null
where null = null //거짓
where 1 = 1 //참
컬렉션 식
컬렉션은 컬렉션 식 이외에 다른 식을 사용할 수 없다.
빈 컬렉션 비교
//JPQL : 주문이 하나라도 있는 회원 조회
select m from Member m
where m.orders is not empty
컬렉션 식이 아닌 경우
select m from Member m
where m.orders is null //오류
컬렉션 멤버 식
[NOT] MEMBER [OF] 컬렉션 값 연관 경우
select t from Team t
where :memberParam member of t.members
스칼라식
숫자, 문자, 날짜, case, 엔티티 타입같은 기본적인 값
날짜함수
- CURRENT_DATE : 현재 날짜
- CURRENT_TIME : 현재 시간
- CURRENT_TIMESTAMP : 현재 날짜 시간
CASE 식
특정 조건에 따라 분기할 때 CASE식 사용
4가지 CASE식
- 기본 CASE
- 심플 CASE
- COALESCE
- NULLIF
기본 CASE
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
심플 CASE
자바의 switch case문과 비슷
select
case t.name
when '팀A' then '인센티브 110%'
when '팀B' then '인센티브 120%'
else '인센티브 105%'
end
from Team t
COALESCE
스칼라식을 차례대로 조회해서 null이 아니면 반환
// m.username이 null이면 '이름없는 회원`을 반환
select coalesce(m.username, '이름없는 회원') from Member m
NULLIF
두 값이 같으면 null 반환, 다르면 첫번째 값. 보통 집합 합수에 사용.
select NULLIF(m.username, '관리자') from Member m
기타 정리
- enum은 == 비교 연산만 지원
- 임베디드 타입은 비교를 지원하지 않는다.
Named 쿼리
어플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해준다.
오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용.
성능상 이점.
@NamedQuery 어노케이션이나 XML에 작성 가능
Named 쿼리를 어노테이션에 정의
정의
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {
...
}
사용
List<Member> resultList = em.createNamedQuery("Member.findByUsername"),
Member.class)
.setParameter("username", "회원1")
.getResultList();
2개 이상 정의
@NamedQueries 어노테이션 사용
@Entity
@NamedQueries({
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"),
@NamedQuery(
name = "Member.count"
query = "select count(m) from Member m")
})
public class Member { ... }