본문 바로가기

개발/JPA

[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 호출

'개발 > JPA' 카테고리의 다른 글

[JPA] week8  (0) 2022.02.13
[JPA 활용2 스터디] week 7  (0) 2022.02.06
[JPA 기본] 9. 값 타입  (0) 2022.01.22
[JPA 기본] 8. 프록시와 연관관계 정리  (0) 2022.01.22
[JPA 기본] 7. 고급 매핑  (0) 2022.01.22