LostCatBox

(JPA) JPA-Basic-CH10(fetch-join)

Word count: 936Reading time: 5 min
2023/04/26 Share

페치 조인

  • SQL 조인 종류X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
    페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

엔티티 페치 조인

  • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에) SQL을 보면 회원 뿐만 아니라 팀**(T.*)*도 함께 *SELECT
  • [JPQL] select m from Member m join fetch m.team
    • 마치 member만 조회한것같지만, 포함된 team까지 모든 연관된 것 영속화됨
    • [SQL]
      SELECT M., *T.*** FROM MEMBER M
      INNER JOIN TEAM T ON M.TEAM_ID=T.ID

스크린샷 2023-04-26 오후 10.20.52

  • 해당부분 Lazy로만 처리한다면, N+1 문제 발생함 => fetch join으로 해결가능

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
  • [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’

일대다 조인은 데이터가 예상보다 많아진다(!!!)

  • 당연히 join의 원리를 이해하면 된다.

스크린샷 2023-04-26 오후 10.32.28

스크린샷 2023-04-26 오후 10.36.33

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령

스크린샷 2023-04-26 오후 10.38.19

  • JPQL의 DISTINCT 2가지 기능 제공
    1. SQL에 DISTINCT를 추가 (join된 결과의 행의 내용이 전부다 같을때만 제거)
    2. 애플리케이션에서 엔티티 중복 제거 (아래 그림처럼 엔티티입장에서 참조값 같으면 JPA가 중복 제거 해준다.)

스크린샷 2023-04-26 오후 10.39.39

DISTINCT가 추가로 애플리케이션에서 중복 제거시도

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

당연히 다대일 관계는 데이터가 예측적이다.

페치 조인과 일반 조인의 차이

일반 조인

  • JPQL은 결과를 반환할 때 연관관계 고려X
  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐 (join 대상의 영속화 X)
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
  • 예시
    • [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 WHERE T.NAME = ‘팀A’

페치 조인

  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(=즉시 로딩)(연관된 join 대상 영속화됨)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
  • 예시
    • [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’

페치 조인의 특징과 한계(!!!)

  • 즉 fetch join 의 목적은 연관된 모든 정보를 모두 가져오는것, where문을 통해 특정 인자만 가져오는것이 목적안됨. 따라서 별칭을 주고 where조건도 불가 -> 즉, 따로 조회해야함

  • 페치 조인 대상에는 별칭을 줄 수 없다.

    • 하이버네이트는 가능, 가급적 사용X
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다. => 1*N*M 경우의수 너무많다!

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

    • 데이터가 엄청 커짐(예측하지못함) -> 일대다로 조회하면,, 같은 데이터 여러개 생기기때문
    • 객체 그래프 사상에도 결국 페치 조인은 모든 데이터다 가져옴. 즉 일대다의 다 데이터를 모두 가져옴 (페이징자르는것 못함, 100만건 한번에 다들고올수도..)
    • 일대일,다대일 같은 단일값 연관필드들은 페치조인해도 페이징가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
  • 내가 만약에 따로 조작할때, fetch join으로 모 가져와야하는데, where문으로 거르면서 영속화 데이터를 거르는 조회는 결국, 예상하지못한 영속화되지 못한 데이터들에 대해 예측할수없는 오류가능(정합성 이슈)

해당 이슈는 다음과 같은 방식으로 해결가능

  • Join으로만 사용하고 Lazy로딩한후 Batch설정 -> sql문에서 IN으로 내부적으로 활용되므로 쿼리 한개만 일어남
    • 글로벌 세팅 추천
      hibernate.default_batch_fetch_size =100
  • 페치조인을 일대다가아닌 다대일로 변경해서 조회
  • 쿼리 직접 짜기

페치 조인 요약

  • 연관된 엔티티들을 SQL 한 번으로 조회 (성능 최적화)
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용
  • 모든 것을 페치 조인으로 해결할 수 는 없음
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

일반적인 방법 3가지

  • 페치 조인으로 해결
  • 페치 조인으로 가져와 application단에서 정제후 사용
  • sql 쿼리를 짜서 필요한 데이터만 조회해서 DTO를 반환하는 것이 효과적

JPQL - 엔티티 직접 사용

엔티티 직접 사용 - 기본 키 값

  • 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

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

  • 외래키도 또한 fk값을 가르킴

스크린샷 2023-04-26 오후 11.21.56

Named 쿼리 - 에너테이션

Spring DATA JPA은 내부적으로 @Query()를 사용하면 Named쿼리로 등록되서 컴파일 에러로 할수있음.
@Query()사용하자

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점(이미 SQL로 파싱되어있음)에 초기화 후 재사용 (비용 줄음)
  • 애플리케이션 로딩 시점에 쿼리를 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
@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();

JPQL - 벌크 연산(!!!)

  • 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
  • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
    1. 재고가 10개 미만인 상품을 리스트로 조회한다.
    2. 상품 엔티티의 가격을 10% 증가한다.
    3. 트랜잭션 커밋 시점에 변경감지가 동작한다.
  • 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행 (더티체킹 단점)

예제

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원
  • INSERT(insert into .. select, 하이버네이트 지원)
1
2
3
4
5
6
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();

벌크 연산 주의

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
    • 전략 2가지
    • 영속성 컨텍스트 생기기전 벌크 연산을 먼저 실행
    • 벌크 연산 수행 후 영속성 컨텍스트 초기화
CATALOG
  1. 1. 페치 조인
    1. 1.1. 엔티티 페치 조인
    2. 1.2. 컬렉션 페치 조인
      1. 1.2.1. 일대다 조인은 데이터가 예상보다 많아진다(!!!)
      2. 1.2.2. 페치 조인과 DISTINCT
      3. 1.2.3. DISTINCT가 추가로 애플리케이션에서 중복 제거시도
      4. 1.2.4. 당연히 다대일 관계는 데이터가 예측적이다.
    3. 1.3. 페치 조인과 일반 조인의 차이
      1. 1.3.1. 일반 조인
      2. 1.3.2. 페치 조인
    4. 1.4. 페치 조인의 특징과 한계(!!!)
      1. 1.4.1. 해당 이슈는 다음과 같은 방식으로 해결가능
    5. 1.5. 페치 조인 요약
      1. 1.5.1. 일반적인 방법 3가지
  2. 2. JPQL - 엔티티 직접 사용
    1. 2.1. 엔티티 직접 사용 - 기본 키 값
    2. 2.2. 엔티티 직접 사용 - 외래 키 값
    3. 2.3. Named 쿼리 - 에너테이션
    4. 2.4. JPQL - 벌크 연산(!!!)
      1. 2.4.1. 예제
      2. 2.4.2. 벌크 연산 주의