*김영한님의 JPA 기본편을 기반으로 작성하였습니다.
jpql 경로 표현식
미리 보는 결론: "묵시적 join 사용하지말고 전부 명시하자!"
( 복잡한 db에서 묵시적 join이 발생 시, 인과관계 파악이 어려워진다. )
상태 필드(state field): 경로 탐색의 끝, 탐색X
ex) m.username. -> username이 마지막, 더 내려갈 수 없다.
단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O
select m.team from Member m;
-> Member와 Team을 join 한 뒤, select(projection) 으로 team을 가져온다(묵시적 join 발생).
이와 같은 묵시적 join이 발생 되지 않게 주의하자.
컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색X
select t.members from Team t; // members는 team 과 member간 1대 다 관계 저장하는 컬렉션
이것 역시 member가 자동으로 join 된다. 하지만 이렇게 가져오게되면 t.members.username 과 같이 더 깊이 들어 갈 수 없게된다.
FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통 해 탐색 가능
select m from Team t join t.members m;
-> m.username 으로 가져올 수 있다.
fetch join: jpql에서 성능 최적화를 위해 제공하는 기능. 연관된 엔티티나 컬레션을 SQL 한번에 함께 조회 가능.
실무에서 필수적인 요소이다.
기본적으로 우리는 Lazy fetch 전략을 사용하기에 member에서 Lazy fetch로 Team을 가져온다 가정해보자
String query = "select m from Member m";
em.createQuery(query,Member.class);
.....................
System.out.println(member.getTeam().getName()); // 연관 되어있는 Team을 사용
//-> Lazy fetch 이기에 Team은 프록시다. 프록시 초기화 발생( 쿼리 발생 )
Lazy 방식이기에, Member
만약 team 내의 멤버 N명의 이름을 출력하게되면, N번의 fetch를 통해 N명의 정보를 가져오게 된다.
-> fetch join으로 성능향상 가능.
String query = "select m from Member m join fetch m.team" //한번에 team도 같이 가져온다.
em.createQuery(query,Member.class);
.....................
System.out.println(member.getTeam().getName()); //앞선 fetch join 으로 실제 Team을 가져옴. 프록시 x
member와 Team을 한번에 가져온다. 따라서 뒤에 나오는 Team은 프록시가 아니고, 그렇기에 프록시 초기화( 실제 엔티티 가져오기)를 수행하지 않아도 되기에 부가적인 쿼리가 발생하지 않는다.
Lazy fetch: 실제 사용되기 전까진 연관된 엔티티를 영속성 컨텍스트에 올리지 않는다.
join fetch: 연관된 모든 엔티티를 영속성 컨텍스트에 올린다. ( Eager fetch 처럼)
** 기본을 Lazy로 두고, 필요할 때 fetch join 으로 한번에 조회하면 된다!
컬렉션의 페치 조인
일 대 다 정보를 담은 컬렉션의 fetch join 시 문제점 ( 일 대 다 에서의 중복 데이터 포함 문제)
** Hibernate 6 이후에선 자동으로 애플리케이션 중복 제거가 적용되기에 상관 없다.
동일한 팀에 소속된 2명의 인원이 그대로 나타난다.
JPA입장에선 (객체 관점) 중복된 사항을 알 수 없다. -> distinct로 제거해야한다.
하지만 SQL의 distinct만으론 모든 중복 제거 불가.
같은 Team A 이지만, 다른 회원 정보를 담고 있기에, 중복제거가 성립되지 않는다.
따라서 JPQL의 DISTINCT는 2가지 기능을 제공한다.
SQL에 Distinct 추가 + 애플리케이션에서 엔티티 중복 제거
애플리케이션에서 올라올 때, 식별자( 팀 id) 로 중복 제거를 수행한다.
1) 그냥 join 사용 시: Lazy fetch 사용 시, Team을 당장 가져오지 않는다.
2) fetch join 사용 시: 연관된 모든 엔티티를 가져온다. (즉시 로딩과 유사하게 동작, 객체 그래프를 SQL 한번에 조회하는 개념)
Fetch Join의 한계
** fetch join의 목적은 "객체 그래프 전체 탐색" 이다.
만약 members 중 특정 멤버 3명을 뽑고 싶다고
select t FROM Team t join fetch t.members where t.name="A";
이런식으로 기존 컬렉션 중 몇가지만 선택하게 되면, 문제가 발생할 수 있다. 따라서 가급적이면 페치 조인 대상에게 별칭을 부여하지 말자.
컬렉션 ( 일 대 다) 에서 페치 조인을 수행 시, 중복된 사항을 거를 수 없기에 페이징 API를 사용할 수 없다.
해결법:
1)방향을 뒤집어서 일 대 다 -> 다 대 일 이 되게 ( 중복 문제 제거 )
select t From Team t join fetch t.members m // 일 대 다
select m From Member m join fetch m.team // 다 대 일
2) fetch join 을 포기하고, Lazy로 인해 일 대 다 에서 개수만큼 SQL을 날려 꺼내오는 과정을 @BatchSize로 완화한다.
lazy 로딩 할 때, 한번에 100개씩 넘겨주게하는 설정. -> Lazy로 한번 끌어올 때마다 100개씩 끌어온다. 이를 통해 fetch join을 사용하지 않고, 어느정도 문제가 완화된다.
페치 조인 정리
모든 것을 페치 조인으로 해결할 수 는 없음
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
- 엔티티 fetch join - join 쓰고 애플리케이션에서 DTO로- 애초에 jpql을 짤 때 new 오퍼레이션으로 DTO로 가져온다.
다형성 쿼리
![](https://blog.kakaocdn.net/dn/wwjBH/btr3HWGbtHr/3DjQK1De9wOrRDYiky0kl1/img.png)
![](https://blog.kakaocdn.net/dn/t7iCw/btr3eX1Ipje/qWSTvs3BQOCksxWlsKfpEk/img.png)
jpql에서 엔티티를 직접 사용하게 된다면?
엔티티의 기본 키 값으로 자동 변환된다. 엔티티는 DB에 넘어가면 결국 P.K 값으로 변환됨
ex) Member m1, Member m2 로 별칭이 정해져있을 때
m1 = m2 -> 둘의 P.K가 유사한지 비교하게 된다.
외래 키도 마찬가지로, Member에서 연관된 Team을 엔티티로 사용시 ( m.team ) Member에서 해당하는 Team 에 접근할 수 있는
F.K값으로 자동 치환된다.
named 쿼리: 쿼리에 이름을 부여하는 것.
-> 미리 쿼리에 이름을 부여해 두고, 재활용가능.( 정적 쿼리만 가능)
"애플리케이션 로딩 시점에서 초기화 후 재사용 한다, 즉 로딩 시점에 쿼리를 검증한다." 이것이 엄청난 메리트로 작용한다.
"selectfdsfwaegg m from Member m" <- 쿼리는 String으로 들어가고, 애플리 케이션 로딩 시점이 아닐 때 이것 처럼 쿼리 문법 문제는 컴파일 타임에 잡아낼 수 없다. -> 애플리케이션 로딩 시점에 쿼리 검증시 잡아낼 수 있다.
만약 Spring Data JPA를 사용한다면 , 이처럼 유용한 named 쿼리를 쉽게 사용 가능
@Query는 named 쿼리를 사용하는 어노테이션. 따라서 "쿼리의 문법오류를 컴파일 타임에서 잡아준다"
벌크 연산
SQL 다수의 update delete. P.K 1건만 찍어서 update delete 하는 sql문 제외한 모든 연산들.
JPA 변경 감지 기능: 이거로 모두 처리하려면 너무 성능이 나빠진다.
매번 변경 감지해서 1개씩 처리 -> 한번에 처리하자 == 벌크 연산
한번에 모든 멤버들의 나이를 20살로 변경하기.
결과 sql
한번의 sql 로 모든 업데이트를 처리.
**주의사항
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
따라서, 과거의 데이터가 영속성 컨텍스트에 들어있고, 그걸 물고 올 가능성이 있다.
해결 방안:- 벌크 연산을 먼저 실행 (벌크 연산을 수행할 때 영속성 컨텍스트에 "벌크 연산" 작업만 수행하자. jpql이기에, 자동으로 flush 되어 영속성 컨텍스트 내의 내용이 Db에 반영되어 데이터 손실 걱정할 필요 없다).
단, flush는 영속성 컨텍스트 내의 데이터를 저장만 하는 것이지, 영속성 컨텍스트를 비우는 것이 아니기에, update 후 해당 값을 불러오더라도, 업데이트 된 값이 아니라 영속성 컨텍스트에 존재하는 과거의 값을 물고 올 수 있다. ( Bad! 따라서 이 방법 비추천)
- 벌크 연산 수행 후 em.clear()로 영속성 컨텍스트 초기화 [권장] ( 만약 애플리케이션에서 과거의 금액값 (5000) 을 가져왔는데, 이후 update 로 인해 데이터의 값이 6000이 된 경우 문제. 영속성 컨텍스트 초기화 시 과거에 읽었던 5000이라는 수치가 날라가고, 다시 6000이라는 값을 가져오는 작업을 수행하게 됨으로써 해결된다.
Spring data JPA에서의 벌크 연산
![](https://blog.kakaocdn.net/dn/5ww5l/btr3oftO40G/EKDV3OxsAL4KbJ1OfZ7kw1/img.png)
-> 벌크 연산 시 앞서 말한 문제점이 남아있다. ( entity manager 을 매번 clear 하는 것도 나름의 문제 존재 )
여기서 @Modifying(clearAutomatically= true) 로 설정값을 넣어주면, 벌크 연산을 수행할 때마다 entity manager을 초기화해준다.
'Spring boot' 카테고리의 다른 글
[스프링 MVC] Part2 (서블릿) (1) | 2023.03.14 |
---|---|
[스프링 MVC] Part 1 (웹 어플리케이션의 이해) (0) | 2023.03.14 |
[JPA] Part 5 (객채지향 쿼리 언어 pt.1) (0) | 2023.03.13 |
[JPA] Part 4 (값 타입) (0) | 2023.03.10 |
[JPA] Part 3 프록시와 연관관계 (0) | 2023.03.10 |