JPA를 공부하자 04 - JPQL 기본편
JPQL
JPA 단순 문법만으로, 실무에서는 사용 불가하다.
find()
만으로 개발을 할 수는 없을 것이다.
예를 들어, select
하는 값이, 엔티티가 아닌 값일 수도 있다.
또 필요한 데이터 만을 조회하기위해서, 검색 조건을 변경해야 할 때도 있다.
따라서, JPA는 SQL을 추상한 JPQL
이라는 객체지향 쿼리를 제공한다.
SQL문법과 유사하지만,
JPQL은 객체 대상의 쿼리, SQL은 데이터베이스 테이블을 대상으로하는 쿼리이다.
JPQL로 쿼리를 짜면, JPA가 SQL로 번역을해서 쿼리를 전송한다.
따라서 JPQL은 특정 DB에 의존적이지 않고,
JPA 방언 설정에 따라 알아서 번역되어 SQL 쿼리를 요청한다.
<br>
JPA의 쿼리
JPA는 다양한 쿼리 방법을 지원한다
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
// SQL
- MyBatis, SpringJdbcTempalte
JPA Criteraia, QueryDSL은 JPQL 쿼리를 자바 코드로 구현 할 수 있도록 하는 것이다.
하지만 JPA Criteria는 코드가 지저분 하므로, 잘 사용하지 않는다.
보통은 JPQL, Spring Data JPA, QueryDSL을 실무에서 주로 사용하고,
실무에서는 JPQL, QueryDSL 조합으로 95%는 가능하다.
하지만, 때때로 정말 불가능한 쿼리를 짜야 할 때, MyBatis, NativeQuery, SpringJdbcTemplate을 이용한다.
JPQL
모든 개념이 그렇듯 , 원리를 이해해야 활용을 할 수 있을것이다.
Spring Data JPA
, QueryDSL
은 결국 JPQL
을 좀더 쉽게 짤 수 있는 uitlity
일 뿐이고, JPQL
을 이해한다면, utility
는 충분히 활용 할 수 있을 것이다.
- 엔티티의 속성은 대소문자 구분. (
Memeber
,age
) - JPQL 키워드는 대소문자 구분X (
SELECT
,from
) - 엔티티 이름 사용, 테이블이름이아니다!
- 별칭은 필수 (
as
생략가능) - ex)
select m from Member mwhere m.age > 18
<br>
다음부터, JPQL의 문법을 정리해보자
Member : Team = 1 : N
관계이다.
<br>
집합과 정렬
select
COUNT(m),
SUM(m.age),
AVG(m.age),
MAX(m.age),
MIN(m.age)
from Member m
<br>
TypeQuery, Query
- TypeQuery : 반환타입 명확
- Query : 반환타입 명확하지않음
TypedQuery<Member> query = em.createQuery("SELECT m From Member m", Member.class);
Query query = em.createQuery("SELECT m.username, m.ageFrom Member m" );
<br>
결과 조회 API
query.getResultList()
: 결과 하나 이상일떄, 결과가 없으면 빈 리스트 반환.query.getSingleResult()
: 결과가 정확히 하나가 아닐경우Exception
이 발생한다. 이 때문에,Spring Data JPA
는 이를 추상화하여,null
,Optional
을 사용한다.
<br>
SetParameter
em.createQuery("select m from Member m where m.username = :useranem", Member.class)
.getParameter("username", "podo")
.getSingleResult();
위치값
을 이용해서, setParameter
를 쓰지말자.
<br>
프로젝션
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
로 중복 제거
<br>
프로젝션 JOIN
JPQL
은 JPA에 의해 SQL
쿼리로 번역되서 전송된다.
예를 들어, 다음 JPQL
쿼리를 보자.
SELECT m.team From Member m
이 쿼리는 실제로 Member
안에 Team
엔티티를 조회하게 되고,
번역된 SQL
은 JOIN
쿼리가 전송하게 된다.
하지만, 단순히 봤을 때는 JOIN
쿼리가 전송되다는 것을 확인하기 어렵다.
JOIN
은 항상 성능이슈의 첫번째 문제이다. 개발자도 모르게 JOIN
쿼리가 전송된다면, 나중에 성능이슈가 분명히 발생할 것이다. JPQL
쿼리를 보고, 나는 알아도 다른 개발자는 모를 수도 있다.
따라서, JPQL
을 SQL
과 최대한 유사하게 짜는 것을 권장하다. 권장 x 100
위에 JPQL
쿼리는 다음과 같이 짜야한다.
SELECT t From Member m join m.team t
<br>
프로젝션, 반환값이 명확하지 않을 때.
이 쿼리의 반환값을 어떻게 명시해야할까?
SELECT m.username, m.age from Member m
첫번째로, Object[]
로 조회 할 수 있다.
List result = em.createQuery("SELECT m.username, m.age from Member m").getResultList();
Object[] obj = result.get(0);
obj[0] // username
obj[1] // age
<br>
두번째로, 새로운 dto
를 정의한다.
생성자를 정의해야하고, dto
의 패키지명까지 다 적어주어야 한다
@AllContructor
class MemberDto{
String username;
Integer age;
}
List<MemeberDto> result = em.createQuery("SELECT new package.MemeberDto(m.username, m.age) from Member m", MemeberDto.class).getResultList();
<br>
페이징
JPA의 아트의 경지!
setFistResults()
: 조회 시작 위치setMaxResults()
: 조회할 데이터수
끝
<br>
JOIN
SELECT m FROM m Member m JOIN m.team t
//내부조인SELECT m FROM m Member m LEFT JOIN m.team t
// 외부조인SELECT m FROM Member m, Team t where m.username = t.name
//세타조인(관계없는 엔티티와 조인)
<br>
JOIN - ON절
JPA 2.1부터 지원한다.
- 조인 대상을 필터링할 수있다.
- 연관관계 없는 엔티티끼리 외부조인을 할 수 있다.(Hibernate 5.1부터), 기존에 세타조인은
inner JOIN
만 가능했다.
조인 대상 필터링
ex) SELECT m FROM m Member m JOIN m.team t on t.name = 'A'
연관관계 없는 엔티티 외부조인
ex) SELECT m FROM Member m LEFT JOIN Team t ON m.username = t.name
<br>
서브쿼리
나이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
메인쿼리의 Member
와 서브 쿼리의 Member
와 달리해야
성능이 잘나오는 것을 명심하자.
EXISTS
, ALL
, ANY
, IN
등 모두 사용가능하다.
<br>
JPA 서브 쿼리의 한계
JPA는 WHERE
, HAVING
절에서만 서브쿼리를 사용할 수있다.
FROM
절에서는 서브쿼리 사용이 불가능하다, 대부분 조인으로 풀어서 해결 할 수있다. 그래도 사용해야 하는 상황이 생긴면, NavtiveQuery 또는, 어플리케이션 단에서 쿼리를 두번 날리는 방법등으로 해결.
<br>
JPA 기타
예를 들어 Member
가 enum
값을 가지고 있다면, 다음과 같이 사용 할 수 있다.
SLECT m.username FROM Member m where M.type = package.MemberType.ADMIN
em.createQuery(`SLECT m.username FROM Member m where M.type = :userType).setParameter("userType", MemberType.ADMIN);
상속관계에서 다음과 같이 사용 할 수 도있다. (잘 쓰지는 않음)
em.createQuery("select i from Item i where type(i) = Book", Item.class);
또한, EXISTS
, IN
, AND
, OR
, NOT
, >
, <
등등 기본적인 문법은 사용 할 수 있다.
<br>
JPA CASE식
기본 CASE식
select
case when m.age <= 10 then `학생요금`
case when m.age >= 60 then `어른요금`
else `일반요금`
from Member m
<br>
단순 CASE식
select
case t.name
when '팀A' then '인센티브100%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
<br>
COALESCE
: 하나씩 조회해서 null이 아니면, 반환
select coalesce(m.username, '이름없음') from Member m
// username이 없으면,이름 없음
반환
<br>
NULLIF
: 두값이 같으면 null
반환
select nullif(m.username, '홍길동') from Member m
// username이 홍길동이면 null
반환
<br>
JPQL 기본 함수
크게 쓰는 함수를 세가지로 구분 할 수있다.
JPQL에서 제공하는 표준 함수
(데이터베이스에 관계없이 사용할 수 있다)
CONCAT
, SUBSTRING
, TRIM
,
LOWER
, UPPER
, LENGTH
,
LOCATE
, ABS
, SQRT
, MOD
등
SIZE
// 컬렉션의 크기를 반환한다.
INDEX
// 컬렉션에서의 위치를 반환한다 (wite @OrederColumn
), 가능하면 쓰지말자.
<br>
각 DB별로 정의된 DB의존적 함수
다행히도, 데이터베이스마다 기본으로 사용하는 함수는 방언에 정의되있다!
<br>
사용자 정의 함수
, 커스텀 함수를 정의 할 수 있다.
커스컴 방언 클래스
를 정의하고, 해당 방언에 함수를 정의하자.
그리고 그 방언 클래스를 사용하자.
필요할 때 라이브러리 코드를 참조해서 사용해보자.
public class MyH2Dialect extends H2Dialect{
public MyH2Dialect(){
registFunction(blaba)
}
}