Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

객체지향 쿼리 언어(JPQL) - 중급 문법 본문

JPA/JPA

객체지향 쿼리 언어(JPQL) - 중급 문법

studyHub 2024. 9. 16. 00:34

경로 표현식

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

Member 엔티티에 필드로 Team team, List<Order> orders 가 있는 경우

경로 표현식 용어 정리

  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드 (ex: m.username)
  • 연관 필드(association field): 연관관계를 위한 필드 -> 묵시적 내부 조인 발생
    • 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

경로 표현식 특징

  • 상태 필드(state field): 경로 탐색의 끝, 탐색X
  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색 가능
  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색 불가(List로 받아서 인식x, 명시적 조인을 통해 별칭을 얻으면 엔티티 매핑돼서 탐색 가능)
//연관 필드는 묵시적 조인이 일어난다.
select m.team from Member m

select 절에 m.team이 있어서 묵시적 조인 발생
컬렉션 값 연관 경로는 리스트로 받아서 더 이상 탐색이 불가. List의 size 정도 밖에 탐색 불가.
FROM 절에서 명시적 조인을 통해 별칭을 얻으면 엔티티 매핑돼서 탐색 가능

명시 조인, 묵시적 조인

  • 명시적 조인: join 키워드 직접 사용
    select m from Member m join m.team t
  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
    select m.team from Member m
    경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌

실무 조언

가급적 묵시적 조인 대신에 명시적 조인 사용

  • 조인은 SQL 튜닝에 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움. 직관적이게 sql과 jpql을 비슷하게 짜자.

Fetch join 

객체 그래프 탐색을 위한 기능. 즉, 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능. 

SQL 조인 종류가 아니고, JPQL에서 성능 최적화를 위해 제공하는 기능. 

 

일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음!
(단지 SELECT 절에 지정한 엔티티만 조회할 뿐.. join은 하되 projection을 안한다..!)

  • [JPQL] select t from Team t join t.members m where t.name = ‘팀A'
  • [SQL] SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID

페치 조인은 연관된 엔티티를 함께 조회

  • [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

성능 최적화 (N+1 문제 해결)

fetch 조인을 왜 써야할까? 한 방 쿼리를 쓸 수 있기 때문이다! (연관된 엔티티들을 SQL 한 번으로 조회)

  • 즉시 로딩이든, 지연 로딩이든 N+1 문제가 발생한다.
    • 지연 로딩 : select m from Member m 후 사용 시점에 N번 조회
    • 즉시 로딩 : select m from Member m 후 즉시 team의 id로 N번 조회

엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함. 실무에서 글로벌 로딩 전략은 모두 지연 로딩.

최적화가 필요한 곳은 페치 조인 적용

 

※ JPQL이 아니라 전역적으로 Entity에 설정할 땐 @NamedEntityGraph 사용

 

컬렉션 페치 조인

일대다 컬렉션 페치 조인을 하면 결과가 중복됐다. (DISTINCT 추가로 애플리케이션에서 중복 제거 시도)

하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 엔티티 중복 제거가 자동으로 적용된다!

더보기

[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'

팀에 소속된 회원이 두명이라 로우가 2개가 된다

teamname = 팀A, team = Team@0x100

  -> username = 회원1, member = Member@0x200

  -> username = 회원2, member = Member@0x300

teamname = 팀A, team = Team@0x100

  -> username = 회원1, member = Member@0x200

  -> username = 회원2, member = Member@0x300

 

페치 조인과 DISTINCT

JPQL의 DISTINCT 2가지 기능 제공

1. SQL에 DISTINCT를 추가 <- SQL에 DISTINCT를 추가해도 일부 데이터가 달라서 SQL 결과에서 중복 제거 실패

2. 애플리케이션에서 엔티티 중복 제거 <- 같은 식별자를 가진 Team 엔티티 제거

팀 자체는 중복되므로, 엔티티 중복을 제거해준다.

 

 

페치 조인의 특징과 한계

 

1. 페치 조인 대상에는 별칭을 주어 필터링하지 말자.

페치 조인은 객체 그래프 탐색을 위해 의도된 것이다. 별칭을 줘서 필터링해버리면 정합성이 떨어진다.

만약 팀의 회원이 5명인데 3명으로 필터링할 경우, 의도도 안맞고 영속성 컨텍스트 입장에서는 처리가 난감하다.

또는 데이터 삭제, 수정 시 cascade 등이 있으면 의도하지 않은 결과가 나올 수도 있다.

 

2. 둘 이상의 컬렉션은 페치 조인하지 말자. 컬렉션은 하나만 지정하자.

위에서 봤듯 일대다 컬렉션에서는 결과 데이터가 뻥튀기 되므로, 여러 개를 쓰면 예상치 못하게 데이터가 엄청 불어난다.

 

3. 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

일대다 컬렉션은 데이터가 뻥튀기 되므로, 중간에 애매하게 잘릴 수 있어 정합성이 깨진다. (팀A에 회원1,2가 있는데 1만..)

하이버네이트는 경고 로그를 남기고 메모리에서 페이징한다. 즉 쿼리상으로는 페이징 조건이 없다.

WARN: HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

-> DB에선 다 긁어서 메모리에 퍼올리므로.. 엄청 위험

  • 일대다, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능하므로, 방향을 뒤집어 사용해라
  • 아니면 페치 조인을 사용하지 말고 N+1로 가되, BatchSize를 설정해 in쿼리로 한번에 조회

 

모든 것을 페치 조인으로 해결할 수는 없다.

여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 컬럼수가 너무 많아서 네트웍 낭비.

이럴 땐 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.


다형성 쿼리

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

상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용 (자바의 타입 다운 캐스팅과 유사)

예) 부모인 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에서 해당 엔티티의 기본 키 값을 사용

 

  • select count(m) from Member m //엔티티를 직접 사용 
    select count(m.id) from Member m //엔티티의 아이디를 사용
  • select m from Member m where m = :member
    select m from Member m where m.id = :memberId

엔티티 직접 사용 - 외래 키 값

엔티티 직접 사용 시 FK 값을 사용하는 경우

  • select m from Member m where m.team = :team
    select m from Member m where m.team.id = :teamId

Named 쿼리

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

정적 쿼리만 가능하다.

어노테이션, XML에 정의할 수 있다. (XML이 우선. 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.)

 

장점

  • 재사용 가능
  • 애플리케이션 로딩 시점에 초기화 후 재사용
    • 로딩 시점에 JPQL->SQL 파싱 비용 지불
    • 로딩 시점에 쿼리 검증!

 

장점이 많지만, 엔티티에 쿼리를 적어둔다는게 지저분하다.

다만 실무에서는 Spring Data Jpa를 사용하게 될텐데, Spring Data Jpa의 @Query가 Named 쿼리 기반이다!

public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.emailAddress = ?") //이름없는 Named Query
  User findByEmailAddress(String emailAddress);
}

벌크 연산 - executeUpdate()

PK 찍는 단건 변경 외의 연산.

 

JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행.

재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

1. 재고가 10개 미만인 상품을 리스트로 조회한다

2. 상품 엔티티의 가격을 10% 증가한다.

3. 트랜잭션 커밋 시점에 변경감지가 동작한다.

4. 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

 

벌크 연산은 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)

executeUpdate()의 결과는 영향받은 엔티티 수 반환 

UPDATE, DELETE 지원, INSERT(insert into .. select, 하이버네이트 지원)

 

벌크 연산 주의

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

영속성 컨텍스트에 반영되지 않는다. (벌크로 대량 작업하니까 영컨에 넣지 않나보다)

 

따라서

  1. (선행 작업이 없다면) 벌크 연산을 먼저 실행하거나,
  2. (선행 작업이 있다면) 벌크 연산을 수행 후(JPQL이니까 flush됨) 영속성 컨텍스트 초기화

영속성 컨텍스트 초기화해서 다시 반영해야 DB에 있는 값을 읽는다.


참고 자료 & 이미지 출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 님)

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

실전 적용 - API 개발과 성능 최적화  (1) 2024.10.11
실전 적용 시 주의 사항  (2) 2024.09.18
객체지향 쿼리 언어(JPQL) - 기본 문법  (0) 2024.09.15
값 타입  (0) 2024.09.12
프록시와 연관관계 관리  (3) 2024.09.10