[JPA] 쿼리 언어 문법 (기본, 중급)
기본: JPA에서 지원하는 쿼리 방법
- JPQL
: 객체 지향 쿼리
: 동적쿼리를 짜는데 불편. -> 대책: Criteria (실무에서 안 쓴다.)
- QueryDSL
* 특수한 경우만 JDBC API, MyBatis, SpringJdbcTemplate 함께 사용
Note
OneToMany 관계 설정시, 메모리 낭비가 있더라도 바로 초기화하자.
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
@Embedded, @Embedable
1) @Embedded: Entity내 객체에 붙이기 (예. Person 내 Address에)
2) @Embedable: 클래스에 붙이기 (예. Address class 위)
기본: JPQL 문법
기본
- 엔티티와 속성은 대소문자 구분
cf. JPQL 키워드는 대소문자 구분 x
- 별칭 m 필수 (as 생략 가능)
select m from Member as m where m.age > 18
- Group by, having 등 안시 기본 문법 모두 지원
TypeQuery, Query
반환 타입 명확할 때 -> TypeQuery
명확하지 않을 때 -> Query (예. select m.name, m.age ....)
getResultList vs getSingleResult
: 결과 하나 이상 vs 결과 정확히 하나
: 주의
-> getResultList는 결과 없으면 빈 리스트 반환
-> getSingleResult: NoResultException, NonUniqueResultException
-> Spring Data JPA에서는 추상화를 통해, 결과 없으면 null 혹은 optional (Exception 안 터짐)
파라메터 바인딩
이름 기준 vs 위치기준
-> 위치 기준 쓰지 말자 (파라메터 하나 새로 추가하면 순서 다 뒤틀림)
프로젝션
select 절에 조회 대상 지정
- 엔티티 (select m from...)
- 임베디드 타입 (select m.address from....)
- 스칼라 타입 (select m.age, m.name from...)
Note: join
안티패턴: 묵시적 조인
List<Team> result = em.createQuery("select m.team from Member m", Team.class)
.getResultList();
good: 명시적 조인
ist<Team> result = em.createQuery("select t from Member m join m.team t", Team.class)
.getResultList();
여러값 조회
- Query / Objects[] / new 명령어
- new 명령어 사용 예시 (가장 깔끔)
ist<MemberDto> result = em.createQuery("select new jpql.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
Note
toString 만들 때 양방향 연관관계의 객체는 toString 대상에서 제외할것. (StackOverflow 남)
페이징
String jpql = "select m from member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(1)
.setMaxResult(10)
.getResultList();
중첩 depth의 지저분한 쿼리를 (예. mysql) 깔끔하게 만든다.
조인
조인시 주의 사항
- OneToMany 연관관계 맺을 때 fetchJoin 속성을 Lazy로 두자.
종류
- inner join (내부 조인)
- outer join (외부 조인)
- 세타 조인.
조인대상 필터링
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
연관관계 없는 엔티티 외부조인 가능
- JPA 5.x 부터 지원. 기존에는 내부 조인만 지원
- SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
서브 쿼리
정의
쿼리 내 쿼리
select m from Member m where m.age (select avg(m2.aget) from Member m2)
서브쿼리 지원 옵션
- exists / not exists : 존재 여부
- all : 모두 만족
- any / some : 조건 하나라도 만족
- in / not in : 서브쿼리 결과 중 하나라도 만족하면 참
jpa 서브쿼리 한계
- JPA 스펙: where, having 절에서만 사용가능
- hibernate (구현체 일종) 스펙: select 절에서도 가능 (예. select (avg(m2.aget) from Member m2) as avg_age ...)
- from 절의 서브쿼리는 JPQL에서 사용 불가 -> 조인으로 풀어 해결
조건식 (case)
- 기본 CASE 식
: case when m.aget <= 10 then '학생 요금'
- 단순 CASE 식
: case t.name when '팀A' then '인센티브110%'
-> exactly matching
- coalesce: null 아니면 반환
: select coalesce(m.username. '이름 없는 회원') from Member m
- NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
: select NULLIF(m.usernmae, '관리자') from Member m;
JPQL 함수
- 기본 함수
: 안시의 함수 그대로 제공
: 특별 케이스
-> SIZE (예. select size(t.members) from Team t)
-> INDEX -> 사용하지 말자.
- 사용자 정의 함수
: config에 방언 설정
: 예시
select function('group_concat', m.username) From Member m
중급문법
경로 표현식
정의
.을 찍어 객체 그래프 탐색
종류
- 상태 필드 (state field)
:단순히 값을 저장하는 용도
- 연관 필드 (association field)
: 연관관계를 위한 필드
-- 단일 값 연관 필드
: 대상이 엔티티 (m.team)
-- 컬렉션 값 연관 필드
: 대상이 컬렉션 (m.orders)
특징
- 상태 필드(state field): 경로 탐색의 끝. 탐색 x
select m.username from Member m
- 단일값 연관 경로: 묵시적 내부 조인(inner join) 발생. 탐색 o
select m.team.name from Member m -> team에서 다시 name을 탐색
- 컬렉션 값 연관경로: 묵시적 내부조인 발생. 탐색 x.
: (예) select m.members from Team t -> members.xx의 형태로 탐색 불가.
: (대안) select m from Team t join t.members m -> from 절에서 명시적 조인을 사용. 별칭 얻음.
=> 묵시적 조인은 실무에서 쓰지 말자. 쿼리 튜닝이 어려워진다.
패치조인: 개념
-> 실무에서 진짜!!! 중요함
- SQL 조인 종류 아님.
- JPQL에서 성능 최적화 위해 사용
- 연관 엔티티/컬렉션을 SQL 한번에 함께 조회
- join fetch 명령어 사용
엔티티 패치 조인: ManyToOne
select m from Member m join fetch m.team
-> SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
패치조인 없을 경우 발생하는 N+1 문제 예시
String jpql = "select m from Member m join m.team"
// member 조회 쿼리 1번
List<Member> members = em.createQuery(jpql. Member.class)
.getResultList();
// for 문 돌면서, 영속성 context에 없는 team은 쿼리 날려서 조회
for ( Member member : result) {
System.out.println(member.getTeam().getName());
}
-> 대안: select m from Member m join fetch m.team
엔티티 패치 조인: OneToMany / 컬렉션 패치 조인
select t from Team t join fetch t.members where t.name = "팀A"
-> SELECT T.*, M.*
FROM TEAM T I
NNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
주의사항
팀A에 2명, 팀 B에 1명일 경우, 위 쿼리를 실행, 결과값을 for문 돌리면 3줄이 나온다. (데이터 뻥튀기 현상)
-> 해결: DISTINCT
DISTINCT
- SQL: 중복된 결과 제거
- JPQL: (1) SQL에 DISTINCT 추가 (2) 애플리케이션의 엔티티 중복 제거 (=같은 식별자 지닌 엔티티 제거)
예) select distinct t from Team t join fetch t.members
-> (1)에서 distinct 해도 실패(row 결과값은 다르니까). (2)에서 distinct 적용됨.
- SQL: 중복된 결과 제거
- JPQL: (1) SQL에 DISTINCT 추가 (2) 애플리케이션의 엔티티 중복 제거?
예) select distinct t from Team t join fetch t.members
패치조인과 일반 조인의 차이
일반 조인 실행시 연관된 엔티티 함께 조회하지 않는다.
select t
from TEam t join t.members m
where t.name = '팀 A'
->
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M
ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀 A'
패치조인: 특징 & 한계
1) 패치 조인 대상에는 별칭을 줄 수 없다.
예. select t from Team t join fetch t.members m where m.age > 10
이유: 객체 탐색 그래프의 취지에 어긋난다. team은 members를 전부 담고 있어야한다.
일부분만 가져오는 것은 삭제, 업데이트 등의 행위시 잘못된 결과를 가져올 수 있다.
만일 members에 필터릴 조건을 걸고 싶다면 select m from Members where ...로 따로 쿼리 날리는 것이 맞다.
2) 둘 이상의 컬렉션은 패치 조인 할 수 없다.
이유: 데이터가 일:다:다의 관계가 되어, row 수 급증. -> 정확성 x
3) 컬렉션을 패치 조인하면 페이징 API (setFirstResult, setMaxResults) 사용 불가
: 일대일/다대일은 페이징 사용 가능
이유: 데이터 뻥튀기
: 팀A(회원1,회원2), 팀B(회원3)의 데이터에서, 컬렉션 패치 조인 후 페이징,
0번 row 가져올 때, 팀A는 회원 1만 가진 것으로 잘못 추력됨.
: 하이버네이트에서 실행시 경고 남기고 메모리에서 페이징 (위험)
-> 데이터 100만건이면 paging 걸어도, 백만건 다 긁어옴.
WARN: ... firstResult/maxResults specified with collection fetch; applying in memory!
해결방법
- ManyToOne 쿼리로 대체
- One에 해당하는 Entity에만 Paging 걸고, ManyToOne은 @BatchSize 사용 (강의 15: 56)
정리
실무에서 글로벌 로딩 전략은 모두 지연 로딩 사용하되, 최적화가 필요한 경우 패치 조인 적용
다형성쿼리
타입 지정 가능
엔티티를 직접 사용
JPQL에서 엔티티를 직접 사용시, SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
- select count(m.id) from Member m
- select count(m) from Member m
-> (SQL 실행 결과는 아래 내용으로 같다)
select count(mid) as cnt from Member m
Named 쿼리
- 미리 이름 정의하여 사용하는 JPQL. 정적쿼리
- 사용방법: (1) 애노테이션 (2) XML
- 장점
: 애플리케이션 로딩 시점에 초기화. 캐싱하여 재사용 가능. (JPA-> SQL 컨버팅 비용 절약)
: 애플리케이션 로딩 시점에 쿼리 검증
-> Spring Data JPA에서 @Query("select ~") 로 사용하는 것도 named query의 일종
벌크 연산
- 여러 row 한번에 변경 (updae, delete 지원)
- 하이버네이트는 insert (select ...) into 지원
벌크 연산 주의
- 벌크 연산은 영속성 컨텍스트 무시. 데이터베이스에 직접 쿼리한다.
- 연산 순서 꼬임 방지 방법
방법 1) (영속성 컨텍스트에 아무 작업 없이) 벌크 연산 먼저 수행
방법 2) 벌크 연산 수행 후 영속성 컨텍스트 초기화
방법 2 추가 설명
-> 회원 나이 조회시 10살. 벌크 연산으로 모두 +1살 됨 -> 영속성 컨텍스트에는 여전히 10살로 남아있음. -> 초기화 하지 않으면, DB에는 11살로 나오지만, 애플리케이션에서는 10살로 나오는 오류 발생.
** 영속성 컨텍스트 -> DB 반영 시점
(1) commit (2) 쿼리 날림 (3) flush 호출