애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.
SQL과 문법 유사하며 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 지원한다.
예시 (기본)
예시 (조건 검색)
Criteria
동적 쿼리 생성을 도와준다.
문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
JPQL 빌더 역할을 한다.
JPA 공식 기능이다.
너무 복잡하고 실용성이 없다.
Criteria 대신에 QueryDSL 사용 권장
예시
QueryDSL
문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
JPQL 빌더 역할을 한다.
컴파일 시점에 문법 오류를 찾을 수 있다.
동적 쿼리 작성하기에 편리하다.
단순하고 쉽다.
실무 사용 권장 (★)
예시
네이티브 SQL
JPA가 제공하는 SQL을 직접 사용하는 기능
JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능을 위해서 사용한다.
예시
오라클 CONNECT BY
특정 DB만 사용하는 SQL 힌트
JDBC 직접 사용, SpringJdbcTemplate 등
JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 처리가 필요가 필요하다.
JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
기본 문법과 쿼리 API
JPQL은 객체지향 쿼리 언어다.
테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.
기본 문법
엔티티명과 속성명은 대소문자를 구분한다.
만약 엔티티명이 Member일 때 member라고 명시하면 오류가 발생한다.
JPQL 키워드는 대소문자를 구분하지 않는다.
SELECT, FROM, where 모두 사용가능한 키워드들이다.
테이블명이 아닌 엔티티명을 사용한다.
별칭은 필수로 설정해야 한다.
AS는 생략해도 된다.
예시
select m from Member as m where m.age > 18
select m.id, m.age, m.username from Member as m where m.age > 18
TypeQuery과 Query
TypeQuery
반환 타입이 명확할 때 사용한다.
예시
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
Query
반환 타입이 명확하지 않을 때 사용
예시
Query query = em.createQuery("SELECT m FROM Member m");
결과 조회 API
query.getResultList()
결과가 하나 이상일 때는 리스트를 반환한다.
결과가 없으면 빈 리스트를 반환한다.
query.getSingleResult()
결과가 정확히 하나일 때 단일 객체를 반환한다.
결과가 없는 경우
javax.persistence.NoResultException 발생
둘 이상인 경우
javax.persistence.NonUniqueResultException 발생
파라미터 바인딩
이름 기준
변수명을 기준으로 파라미터를 바인딩한다.
예시
위치 기준
위치를 기준으로 파라미터를 바인딩한다.
위치는 절대적인 것이 아니기 때문에 절대 사용하면 안된다.
예시
프로젝션 (SELECT)
프로젝션이란?
SELECT 절에 조회할 대상을 지정하는 것
프로젝션 대상
엔티티
예시
Member는 회원 엔티티다.
Team은 팀 엔티티다.
SELECT m FROM Member m
SELECT m.team FROM Member m
임베디드 타입
예시
address는 주소에 대한 임베디드 타입이다.
SELECT m.address FROM Member m
스칼라 타입(숫자, 문자등 기본 데이터 타입)
예시
username은 회원명을 가리키는 String이다.
age는 회원의 나이를 가리키는 int다.
SELECT m.username, m.age FROM Member m
DISTINCT 키워드로 중복을 제거할 수 있다.
여러 가지 값을 조회할 때의 처리 방법은 3가지가 있다.
Query 타입으로 조회
Object[] 타입으로 조회
new 명령어로 조회
단순 값을 VO(DTO)로 바로 조회
예시
SELECT new com.example.demo.vo.step9.UserVO(m.username, m.age) FROM Member m
패키지 명을 포함한 전체 클래스명을 입력해야 한다.
순서와 타입이 일치하는 생성자가 필요하다.
페이징
JPA는 페이징을 다음 두 API로 추상화한다.
setFirstResult(int startPosition)
조회 시작 위치
0부터 시작한다.
setMaxResults(int maxResult)
조회할 데이터 수
예시
조인
조인 종류
내부 조인
예시
SELECT m FROM Member m [INNER] JOIN m.team t
외부 조인
예시
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
세타 조인
예시
select count(m) from Member m, Team t where m.username = t.name
ON절을 활용한 조인
JPA 2.1부터 지원한다.
종류
조인 대상 필터링
연관관계 없는 엔티티 외부 조인
하이버네이트 5.1부터 지원한다.
조인 대상 필터링
예시
전제 조건
회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
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'
연관관계 없는 엔티티 외부 조인
예시
전제 조건
회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL
SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
서브 쿼리
JPA에서는 서브 쿼리도 지원한다.
서브 쿼리도 메인 쿼리랑 마찬가지로 엔티티를 기준으로 작성해야 한다.
관련 SQL 함수
EXISTS (subquery)
서브쿼리에 결과가 존재하면 참
NOT EXISTS (subquery)
서브쿼리에 결과가 존재하지 않으면 참
ALL (subquery)
모두 만족하면 참
예시
나이 > ALL (10, 20, 30)이라면 나이 > 10, 나이 > 20, 나이 > 30을 모두 만족해야 한다.
ANY (subquery) or SOME (subquery)
조건을 하나라도 만족하면 참
예시
나이 > ANY (10, 20, 30)이라면 나이 > 10, 나이 > 20, 나이 > 30 중 하나라도 만족하면 된다.
IN (subquery)
서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
NOT IN (subquery)
서브쿼리의 결과 중 같은 것이 하나도 없으면 참
JPA 서브 쿼리 한계
JPA의 서브 쿼리는 WHERE, HAVING 절에서만 사용할 수 있다.
SELECT절에서도 서브 쿼리를 사용할 수 있다.
하이버네이트에서 지원
현재 JPQL에서는 FROM절에 서브 쿼리를 사용할 수 없다.
조인으로 풀 수 있으면 풀어서 해결해야 한다.
그러나 하이버네이트6부터는 FROM절에서도 서브 쿼리를 사용할 수 있다.
JPQL 타입 표현과 기타식
JPQL 타입 표현
문자
'HELLO'
'She''s'
숫자
Double
10D
Float
10F
Long
10L
Boolean
TRUE
FALSE
ENUM
com.example.demo.MemberType.Admin
패키지명을 포함해야 한다.
파라미터 바인딩 방식을 쓰는 게 편하다.
엔티티 타입
TYPE(m) = Member
상속 관계에서 사용한다.
WHERE절에서 사용한다.
자주 사용되지는 않지만 다형성 쿼리에서는 쓰는 경우가 있다.
기타식
SQL과 문법이 같은 식
EXISTS, IN
AND, OR, NOT
=, >, >=, <, <=, <>
BETWEEN, LIKE, IS NULL
조건식 (CASE 등등)
기본 CASE식
조건식의 결과가 참인 경우의 값을 반환한다.
조건 CASE식
값이 일치하는 경우의 값을 반환한다.
COALESCE
인자로 지정한 요소들을 하나씩 비교해서 NULL이 아니면 NULL이 아닌 첫번째 값을 반환한다..
만약 COALESCE(A, B, C)인 경우에 A와 B가 NULL이라면 C를 반환한다.
NULLIF
두 값이 같으면 null을 반환한다.
두 값이 다르면 첫번째 값을 반환한다.
JPQL 함수
JPGQ 기본 함수
CONCAT
SUBSTRING
TRIM
LOWER, UPPER
LENGTH
LOCATE
ABS, SQRT, MOD
SIZE, INDEX(JPA 용도)
사용자 정의 함수 호출
하이버네이트는 사용전 방언에 추가해야 한다.
사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
예시
select function('group_concat', i.name) from Item i
경로 표현식
경로 표현식이란?
.(점) 연산자를 통해 객체 그래프를 탐색하는 것
관련 용어
상태 필드 (state field)
단순히 값을 저장하기 위한 필드
예시 : m.username
연관 필드 (association field)
연관관계를 위한 필드
단일 값 연관 필드
대상이 엔티티인 경우에 해당한다.
@ManyToOne, @OneToOne
예시 : m.team
컬렉션 값 연관 필드
대상이 컬렉션인 경우에 해당한다.
@OneToMany, @ManyToMany
예시 : m.orders
경로 표현식의 특징
상태 필드 (state field)
이름이나 나이처럼 특정 값을 나타내는 상태 필드는 더 이상 경로 탐색을 할 수 없다.
단일 값 연관 경로
단일 값 연관 필드가 조회 대상에 포함될 경우에 묵시적 내부 조인이 발생한다.
inner join을 의미한다.
추가 탐색이 가능하다.
컬렉션 값 연관 경로
컬렉션 값 연관 필드가 조회 대상에 포함될 경우에 묵시적 내부 조인이 발생한다.
컬렉션 자체를 가리키는 것이기 때문에 추가 탐색이 불가능하다.
FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색할 수 있다.
예시 : select m.username from team t join t.members m
명시직 조인
join 키워드 직접 사용하여 조인한 경우를 의미한다.
예시 : select m from Member m join m.team t
묵시적 조인
경로 표현식에 의해 묵시적으로 조인이 발생하는 경우를 의미한다.
내부 조인만 가능
예시 : select m.team from Member m
경로 표현식 - 예제
select o.member.team from Order o => 성공
select t.members from Team => 성공
select t.members.username from Team t => 실패
select m.username from Team t join t.members m => 성공
경로 탐색을 사용한 묵시적 조인 시 주의사항
경로 탐색에 의한 묵시적 조인 시은 항상 내부 조인이다.
컬렉션 값 연관 필드 경로 탐색의 끝이다.
만약 추가 탐색을 진해앟려면 명시적 조인을 통해 별칭을 얻어야 한다.
경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 FROM (JOIN) 절에 영향을 준다.
TIP
가급적 묵시적 조인 대신에 명시적 조인을 사용해야 한다.
조인은 SQL 튜닝에서 매우 중요한 요소다.
묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다.
추후 유지보수에 매우 곤란함을 제공하기도 한다.
패치 조인
실무에서 매우 중요한 기능이다.
SQL 조인 종류에 포함되지 않는다.
JPQL에서 성능 최적화를 위해 제공하는 기능
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.
join fetch 명령어를 통해 사용한다.
[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
SELECT 절에 지정한 엔티티만 조회한다.
페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다.
즉시 로딩으로 동작한다.
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.
N + 1 문제를 해결할 때 많이 사용된다.
N + 1이란 최초 1번 엔티티 A의 목록을 조회하고 A의 목록에 대해 반복문을 돌리면서 해당 A에 관련된 엔티티 B를 조회할 때 해당하는 B가 영속성 컨텍스트에 존재하지 않아서 SQL을 새로 실행하는 경우가 최대 N번 (A의 개수를 의미) 발생 할 수 있다는 것을 의미한다.
모든 것을 페치 조인으로 해결할 수 는 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면, 페치 조인보다는 일반 조인으로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
엔티티 페치 조인
특정 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
페치 조인으로 함께 조회하기 때문에 즉시 로딩이 된다.
예시
JPQL
select m from Member m join fetch m.team
SQL
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
회원과 팀에 대해서 모두 조회한다.
컬렉션 페치 조인
일대다 관계, 컬렉션 페치 조인
예시
JPQL
select t from Team t join fetch t.members where t.name = 'Team A'
SQL
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = 'Team A'
페치 조인과 DISTINCT
SQL의 DISTINCT
중복된 결과를 제거하는 명령
SQL에서는 DISTINCT를 추가해도 데이터가 조금이라도 다르면 중복이 제거되지 않는다.
JPQL의 DISTINCT
SQL에 DISTINCT를 추가한다.
애플리케이션에서 엔티티 중복을 제거한다.
같은 식별자를 가진 엔티티를 제거한다.
하이버네이트6 변경 사항
DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 자동으로 중복 제거를 시도한다.
예시
select distinct t from Team t join fetch t.members where t.name = 'Team A'
페치 조인의 특징과 한계
페치 조인 대상에는 별칭을 줄 수 없다.
하이버네이트는 가능하다.
가급적 사용하지 않아야 한다.
관례적으로 별칭을 주지 않는다.
둘 이상의 컬렉션은 페치 조인 할 수 없다.
한 번의 일대다도 조회되는 데이터의 수가 급증할 수 있는데 그 이상은 더욱 급증할 수 있다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
방향을 뒤집어서 해결하는 방법도 있다.
기존
select t from Team t join fetch t.members m
변경
select m from Member m join fetch m.team t
@BatchSize를 활용하기도 한다.
하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 한다.
매우 위험한 방법이다.
연관된 엔티티들을 SQL 한 번으로 조회하여 성능 최적화에 도움을 준다.
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선권을 갖는다.
글로벌 로딩 전략은 @OneToMany(fetch = FetchType.LAZY)같은 경우를 의미한다.
실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
최적화가 필요한 곳은 페치 조인 적용한다.
예시 : N + 1
다형성 쿼리
슈퍼 클래스와 서브 클래스 간의 관계에 대하여 조건을 설정한다.
예시를 위해 Item이라고 하는 물품 클래스가 있고, Item 클래스를 상속받는 Album, Book, Movie 클래스가 있다고 가정한다.
type
조회 대상을 특정 자식으로 한정한다.
예시
전제 조건
Item 중에 Book, Movie를 조회
JPQL
select i from Item i where type(i) IN (Book, Movie)
SQL
select i from i where i.DTYPE in ('B', 'M')
treat (JPA 2.1)
자바의 타입 캐스팅과 유사한 방식
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
FROM절과 WHERE절에서 사용한다.
하이버네이트에서는 SELECT도 지원한다.
예시
전제 조건
부모인 Item과 자식 Book이 있다.
JPQL
select i from Item i where treat(i as Book).author = 'kim'
SQL
select i.* from Item i where i.DTYPE = 'B' and i.author = 'kim'
엔티티 직접 사용
기본 키 값
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
예시
JPQL
select count(m.id) from Member m => 엔티티의 아이디를 사용
select count(m) from Member m => 엔티티를 직접 사용
SQL
JPQL 둘다 같은 다음 SQL 실행
select count(m.id) as cnt from Member m
엔티티를 파라미터로 전달
식별자를 직접 전달
외래 키 값
Named 쿼리
미리 정의해서 이름을 부여해두고 사용하는 JPQL
정적 쿼리로만 사용할 수 있다.
파라미터를 설정하는 것은 가능하다.
미리 정의한 쿼리에 추가 내용을 덧붙일 수는 없다.
어노테이션이나 XML에 정의하여 사용한다.
애플리케이션 로딩 시점에 초기화 후 재사용
애플리케이션 로딩 시점에 JPA나 하이버네이트가 SQL로 파싱해서 캐시에 보유하고 있게 된다.