2023. 02.07 17:40 복습 및 정리 시작
JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.
우리는 이제 그중에서도 제일 중요하고 기본이 되는 JPQL에 대해서 알아보겠다.
JPQL
JPQL은 객체지향 쿼리 언어다. JPA를 사용하는 사람들은 필수로 JPQL을 학습해야 한다.
우리가 JPA를 개발하다 보면 em.find()메소드로는 복잡한 검색 쿼리를 데이터베이스에 보낼 수 없다.
따라서 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.
또한 우리는 ORM을 사용해 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다.
즉 엔티티 객체를 대상으로 하는 검색 조건이 포함된 SQL(?)이 필요하다. 쉽게 말해 이것이 바로 JPQL이다.
JPQL은 아래와 같은 특징을 가진다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- SQL 문법과 유사하다.
SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다.
JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다.
그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.
JPQL은 즉 객체지향 SQL이다.
기본 문법과 기능
우선 엔티티를 저장할때는 EntityManager.persist() 메소드를 사용하므로 INSERT 문은 없다.
기초 문법은 아래와 같다. 기존 SQL문과 굉장히 유사하다.
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
집합과 정렬
select
COUNT(m), //회원수
SUM(m.age), //나이 합
AVG(m.age), //평균 나이
MAX(m.age), //최대 나이
MIN(m.age) //최소 나이
from Member m
먼저 SELECT문 부터 살펴보자. 예시를 살펴보자.
select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자를 구분한다. 예를 들어 Member, username은 대소문자를 구분한다.
- 반면 JPQL 키워드 (select, from, as)는 대소문자를 구분하지 않는다.
- 엔티티 이름을 사용한다. 테이블 이름을 사용하지 않는다.
- 별칭은 필수다. Member에 m이라는 별칭을 주었다. 그러나 as는 생략할 수 있다.
TypeQuery와 Query
TypeQuery는 반환 타입이 명확할 때 사용한다. Query는 반환 타입이 명확하지 않을 때 사용한다.
TypedQuery<Member> query = em.createQuery("SELECT m From Member m", Member.class);
Query query = em.createQuery("SELECT m.username, m.age from Member m");
결과 조회에 대한 API
- 결과가 하나 이상일 때, 리스트를 반환하려면 query.getResultList()를 사용한다. 결과가 없으면 빈 리스트를 반환한다.
- 결과가 정확히 하나면, 단일 객체 반환을 위해 query.getSingleResult()를 반환한다. 결과가 없거나 둘 이상이면, 예외가 발생한다. 자세한 건 책을 참조하면 되겠다.
파라미터 바인딩
JPQL은 위치 기준, 위치 기준 파라미터 바인딩을 모두 지원한다.
먼저 이름 기준 파라미터다. 이름 기준 파라미터는 파라미터를 이름으로 구분한다.
이름 기준 파라미터는 앞에 :를 사용한다. 아래 코드를 살펴보자.
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();
위치 기준 파라미터 방식보다는 이름 기준 파라미터 방식을 사용하자.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다.
프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(기본 데이터 타입)이 있다. 아래 코드로 예시를 살펴보자.
SELECT m FROM Member m //엔티티 프로젝션
SELECT m.team FROM Member m //엔티티 프로젝션
SELECT m.address FROM Member m // 임베디드 타입 프로젝션
SELECT m.username, m.age FROM Member m //스칼라 타입 프로젝션
//DISTINCT로 중복 제거
프로젝션을 여러 값 조회하는데는 크게 3가지 방법이 있다.
- Query 타입으로 조회
- Object[] 타입으로 조회
- new 명령어로 조회
단순 값을 DTO로 바로 조회할 때 사용한다. new 명령어를 사용할 때는 패키지 명을 포함한 전체 클래스 명을 입력해야 하고, 순서와 타입이 일치하는 생성자가 필요하다. 예시 코드는 아래와 같다.
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
페이징 API
페이징 처리용 SQL은 반복적이고 지루하며 데이터베이스마다 페이징 SQL문도 다르다.
JPA는 페이징을 다음 두 API로 모두 추상화해 이 API를 사용하면 SQL에 의존적이지 않게 개발을 할 수 있다.
- setFirstResult(int startPosition) : 조회 시작 위치
- setMaxResults(int maxResult) : 조회할 데이터 수
List<Member> list = em.createQuery("select m from Member m order by m.name desc", Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
조인
- 내부 조인
SELECT m FROM Member m [INNER] JOIN m.team t. 참고로 INNER는 생략 가능 - 외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t. OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용. - 세타 조인
SELECT count(m) FROM Member m, Team t where m.username = t.name. 참고로 세타 조인은 내부 조인만 지원.
JPA 2.1부터 조인할 때 ON절을 지원한다. ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
참고로 내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
SELECT m,t FROM Member m left join m.team t on t.name = 'A'
SQL 결과를 보면 and t.name='A'로 조인 시점에 조인 대상을 필터링한다.
서브 쿼리
서브 쿼리란 SQL안에 들어간 또 하나의 SQL문이다. JPQL도 SQL처럼 서브 쿼리를 지원하지만 제약이 있다.
서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.
//나이가 평균보다 많은 회원
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
서브 쿼리는 다음 함수들과 같이 사용할 수 있다.
- [NOT] EXISTS (subquery) : 서브 쿼리에 결과가 존재하면 참
- {ALL | ANY | SOME } (subquery) : 비교 연산자와 같이 사용한다.
- ALL : 조건을 모두 만족하면 참이다.
- ANY OR SOME : 둘은 같은 의미다. 조건을 하나라도 만족하면 참이다.
- [NOT] IN (subquery)
- 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. 참고로 IN은 서브 쿼리가 아닌 곳에서도 사용 가능.
아래는 서브 쿼리 함수 사용 예제다.
팀A 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = '팀A')
전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
어떤 팀이든 팀에 소속된 회원
select m from Member m where m.team = ANY (select t from Team t)
JPQL 타입 표현
종류 | 설명 | 예제 |
문자 | 작은 따음표 사이에 표현. 작은 따음표 두 개 표현은 ''. | 'Hello' , 'She''s' |
숫자 | L (Long), D (Double), F(Float) | 10L, 10D, 10F |
날짜 | DATE {d 'yyyy-mm-dd'} TIME {t 'hh-mm-ss'} DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'} |
{d '2019-03-24'} {t '10-11-11'} {ts '2012-03-24 10-11-11.123'} m.createDate = {d '2019-03-24'} |
Boolean | Boolean : TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름을 사용해야 한다. | jpabook.MemberType.Admin |
엔티티 타입 | 엔티티의 타입을 표현. 주로 상속과 관련해 사용 | TYPE(m) = Member |
조건식 - CASE 식
case 식은 4가지 종류가 있다. 기본 CASE, 심플 CASE, COALESCE, NULLIF가 있다.
먼저 기본 CASE 식이다.
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
단순 CASE 식이다.
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
COALESCE는 하나씩 조회해서 null이 아니면 반환한다.
사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
NULLIF : 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
JPQL은 CONCAT, SUBSTRING 과 같은 다양한 문자 함수와 ABS, SQRT 같은 다양한 수학식도 제공한다.
또한 하이버네이트 구현체를 사용해 방언 클래스를 상속받고, 사용자 정의 함수를 등록할 수도 있다.
책을 읽어보는 것이 큰 도움이 될 것이다.
참고자료
자바 ORM 표준 JPA 프로그래밍 - 김영한
자바 ORM 표준 JPA 프로그래밍 강의 - https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
2023. 02.07 18:20 복습 및 정리 마무리
댓글