페치 조인
- 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
- 해당부분 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’
- SELECT T., M.\ FROM TEAM T
일대다 조인은 데이터가 예상보다 많아진다(!!!)
- 당연히 join의 원리를 이해하면 된다.
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령
- JPQL의 DISTINCT 2가지 기능 제공
- SQL에 DISTINCT를 추가 (join된 결과의 행의 내용이 전부다 같을때만 제거)
- 애플리케이션에서 엔티티 중복 제거 (아래 그림처럼 엔티티입장에서 참조값 같으면 JPA가 중복 제거 해준다.)
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’
- [JPQL]
페치 조인
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(=즉시 로딩)(연관된 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’
- [JPQL]
페치 조인의 특징과 한계(!!!)
즉 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값을 가르킴
Named 쿼리 - 에너테이션
Spring DATA JPA은 내부적으로 @Query()를 사용하면 Named쿼리로 등록되서 컴파일 에러로 할수있음.
@Query()사용하자
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 정적 쿼리
- 어노테이션, XML에 정의
- 애플리케이션 로딩 시점(이미 SQL로 파싱되어있음)에 초기화 후 재사용 (비용 줄음)
- 애플리케이션 로딩 시점에 쿼리를 검증
1 |
|
JPQL - 벌크 연산(!!!)
- 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
- JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
- 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행 (더티체킹 단점)
예제
- 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
- executeUpdate()의 결과는 영향받은 엔티티 수 반환
- UPDATE, DELETE 지원
- INSERT(insert into .. select, 하이버네이트 지원)
1 | String qlString = "update Product p " + |
벌크 연산 주의
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
- 전략 2가지
- 영속성 컨텍스트 생기기전 벌크 연산을 먼저 실행
- 벌크 연산 수행 후 영속성 컨텍스트 초기화