JPA & Querydsl/JPA

8. 객체지향 쿼리 언어(JPQL)

1. 객체지향 쿼리 언어 소개

JPA는 다양한 쿼리 방법을 지원

검색 쿼리에서 JPA는 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색

애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검 색 조건이 포함된 SQL이 필요

 

2. JPQL

JPQL

SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어

SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원

테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리

SQL을 추상화해서 특정 데이터베이스 SQL에 의존X

//검색
 String jpql = "select m from Member m where m.age > 18"; 
 List<Member> result = em.createQuery(jpql, Member.class)
 .getResultList();
실행된 SQL 
	select 
		m.id as id, 
		m.age as age, 
		m.USERNAME as USERNAME, 
		m.TEAM_ID as TEAM_ID 
	from 
		Member m 
	where 
		m.age>18 

JPA Criteria

문자가 아닌 자바코드로 JPQL을 작성할 수 있음

JPQL 빌더 역할, JPA 공식 기능

너무 복잡하고 실용성이 없음.

//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder(); 
CriteriaQuery<Member> query = cb.createQuery(Member.class); 

//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class); 

//쿼리 생성 CriteriaQuery<Member> cq = 
query.select(m).where(cb.equal(m.get("username"), “kim”)); 
List<Member> resultList = em.createQuery(cq).getResultList();

QueryDSL

문자가 아닌 자바코드로 JPQL을 작성할 수 있음

JPQL 빌더 역할

컴파일 시점에 문법 오류를 찾을 수 있음

동적쿼리 작성 편리함

JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member; 
List<Member> list = 
	query.selectFrom(m)
    .where(m.age.gt(18)) 
    .orderBy(m.name.desc())
    .fetch();

네이티브SQL

JPA가 제공하는 SQL을 직접 사용하는 기능

JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능

String sql = “SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’"; 

List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

JDCAPI 직접 사용, Mybatis, SpringJdbcTemplate

영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요

 

3. 기본 문법과 기능

JPQL은 객체지향 쿼리 언어다.따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하 지 않는다. -> JPQL도 결국 SQL로 변환되서 던져짐

select m from Member as m where m.age > 18

엔티티와 속성은 대소문자 구분O (Member, age)

JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)

from 절을 보면 알수 있듯 엔티티 이름 사용

별칭은 필수

 

TypeQuery: 반환 타입이 명확할 때 사용

TypedQuery<Member> query = 
 em.createQuery("SELECT m FROM Member m", Member.class);

Query: 반환 타입이 명확하지 않을 때 사용

Query query = 
 em.createQuery("SELECT m.username, m.age from Member m");

 

query.getResultList(): 결과가 하나 이상일 때, 리스트 반환

query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환 

- 결과가 없으면: javax.persistence.NoResultException

- 둘 이상이면: javax.persistence.NonUniqueResultException

 

파라미터 바인딩 - 이름 기준, 위치 기준

// 이름 기준
SELECT m FROM Member m where m.username=:username 
query.setParameter("username", usernameParam);

// 위치 기준
SELECT m FROM Member m where m.username=?1 
query.setParameter(1, usernameParam);

 

프로젝션

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로 중복 제거

페이징 API

setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)

setMaxResults(int maxResult) : 조회할 데이터 수

//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
	.setFirstResult(10)
	.setMaxResults(20)
	.getResultList();

DB방언 에 맞춰서 쿼리가 생성되어 던져짐(오라클 페이징 쿼리는 언제바도 극혐 ...)

SELECT
	M.ID AS ID,
 	M.AGE AS AGE,
 	M.TEAM_ID AS TEAM_ID,
 	M.NAME AS NAME 
FROM
 	MEMBER M 
ORDER BY
 	M.NAME DESC LIMIT ?, ?
SELECT * FROM
 	( SELECT ROW_.*, ROWNUM ROWNUM_ 
 FROM
 	( SELECT
 		M.ID AS ID,
 		M.AGE AS AGE,
 		M.TEAM_ID AS TEAM_ID,
 		M.NAME AS NAME 
 	FROM MEMBER M 
 	ORDER BY M.NAME 
 	) ROW_ 
 	WHERE ROWNUM <= ?
 ) 
WHERE ROWNUM_ > ?

조인

ON절을 활용한 조인(JPA 2.1부터 지원)

연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

// 내부 조인
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

// 조인 대상 필터링
// JPQL
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'  

// 연관관계 없는 엔티티 외부 조인
SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name

서브쿼리

JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능

SELECT 절도 가능(하이버네이트에서 지원)

FROM 절의 서브 쿼리는 현재 JPQL에서 불가능 - > 조인으로 풀수 있는 경우 풀어서 해결

 

서브 쿼리 지원 함수

[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참

{ALL | ANY | SOME} (subquery)

ALL 모두 만족하면 참

ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참

[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

// 나이가 평균보다 많은 회원
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 

// 팀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)

CASE식, COALESCE, NULLIF

CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, SQRT, MOD, SIZE, INDEX(JPA 용도)

사용자 정의 함수를 지원한다.

4. 경로 표현식

.(점)을 찍어 객체 그래프를 탐색하는 것을 말하는데

상태 필드(state field): 경로 탐색의 끝, 탐색X

단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O

컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색X

- FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통 해 탐색 가능

 

- 명시적 조인: join 키워드 직접 사용

- 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)

 

가급적 묵시적 조인 대신에 명시적 조인 사용 (조인은 SQL 튜닝에 중요 포인트)

select m.username -> 상태 필드
from Member m 
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A' 

경로 표현식 용어

상태 필드(state field): 단순히 값을 저장하기 위한 필드 (ex: m.username)

연관 필드(association field): 연관관계를 위한 필드

- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team) 

- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

 

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 // 성공

5. 페치 조인

JPQL에서 성능 최적화를 위해 제공하는 기능

연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능

페치 조인과 일반 조인의 차이 - 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음

페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

페치 조인 대상에는 별칭을 줄 수 없다. ( 하이버네이트는 가능, 가급적 사용X )

둘 이상의 컬렉션은 페치 조인 할 수 없다. - 이 부분을 몰라서 30분동안 삽질의 경험이 ...

컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능

- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험) -> 거 딱 어플리케이션 죽기 좋은 날씨네 ㅎ

 

글로벌 로딩 전략을 지연 로딩으로 하고 성능 최적화가 필요한 부분에 적용

[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 = ‘팀A' 

[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

 

JPQL의 DISTINCT 2가지 기능 제공

1. SQL에 DISTINCT를 추가

2. 애플리케이션에서 엔티티 중복 제거

7. 엔티티 직접 사용

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

8. Named 쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL

정적 쿼리

어노테이션, XML에 정의 - XML이 항상 우선

애플리케이션 로딩 시점에 초기화 후 재사용

애플리케이션 로딩 시점에 쿼리를 검증

@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();

9. 벌크 연산

JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행 - > 쿼리 한 번으로 여러 테이블 로우 변경(엔티티) 

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리

- 벌크 연산을 먼저 실행

- 벌크 연산 수행 후 영속성 컨텍스트 초기화

String qlString = "update Product p " +
 "set p.price = p.price * 1.1 " + 
 "where p.stockAmount < :stockAmount"; 
 
int resultCount = em.createQuery(qlString) 
 .setParameter("stockAmount", 10) 
 .executeUpdate(); 

Reference

자바 ORM 표준 JPA 프로그래밍 - 김영한님

'JPA & Querydsl > JPA' 카테고리의 다른 글

7. 값 타입  (0) 2021.02.28
6. 프록시와 연관관계 관리  (0) 2021.02.25
5. 고급 매핑  (0) 2021.02.24
4. 다양한 연관관계 매핑  (0) 2021.02.24
3. 연관관계 매핑 기초  (0) 2021.02.22