1. 경로 표현식
* 경로 표현식 - 객체 그래프 내를 탐색하기 위한 필드 표현 방식
상태 필드 (State Field) > 단순 값 저장 필드 : m.username (m = alias 준다고 표현)
연관 필드 (AssoociationField) > 연관관계를 위한 필드 : 단일 값 연관 필드(경로) / 컬렉션 값 연관 필드
*상태 필드
상태 필드 >> 경로 탐색의 끝. 탐색 종료.
예를 들자.
String query = "select m.username from JpqlMember m";
*단일 값 연관경로
username.~ 가 더이상 어디서 갈 수가 없음. 상태필드를 만나게 되면 탐색의 끝.
String query = "select m team from JpqlMember m";
연관관계 (OneToOne 혹은 ManyToOne 으로 매핑된 객체에 대한 탐색) 을 단일 값 연관경로라고 한다. 이 때 묵시적 inner join 이 발생하고, 지속 탐색이 가능하다. ex) m.team.name
select (team data) from member m inner join team t on m.team_id = t.team_id; 의 쿼리가 난다. 어찌보면 당연한 거임. DB에서는 객체 탐색을 하려면 당연히 JOin 이 발생함. 다 알고 있던 것.
>> 이러면 느낌이 와야 함 :: 이거 조심해야함.
>> 묵시적 내부 조인은 최대한 사용하면 안됨 >> 쿼리 튜닝이 어려움 !
* 컬렉션 값 연관경로
1대 다를 말하는것, 위와 반대 상황
String query = "select t.members from JpqlTeam t";
Collection result = em.createQuery(query, Collection.class).getResultList();
for(Object o : result){
System.out.println("o = " + o);
}
1개가 아님 > 가지고 있는 멤버가 여러개.
SQL: select (member datas) from Team t inner join Member m on t.team_id = m.team_id;
t.members > 컬렉션으로 들어옴. 멤버 내부에 대한 탐색은 불가능하다!! >> 더 이상 탐색하지 못함.
"묵시적인 inner join 을 하지 말고, 명시적인 inner join 을 해라"
명시적 inner join 을 하면 alias 세팅을 할 수 있잔아
select m from JpqlTeam t join t.member m
뿐만 아니고 쿼리를 튜닝하기가 훨씬 용이. 객체에 의존하지 말고, 필요한 SQL을 먼저 짜면서 JPQL 을 명시적으로 세팅해라. SQL 튜닝을 하는 것이 정말 중요하다고 함.
2. Fetch Join (100 % 이해를 꼽아 놔야 함)
실무에서 제일 중요. JPQL 의 꽃. ㅈㄴ 중요.
SQL 조인의 종류가 아님. JPQL 에서 성능 최적화를 위해 제공하는 기능.
연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능.
select m from Member m join fetch m.team; // Member 의 모든 column 을 가져오되, 연관 객체 team 도 같이 Fetch 해라
> 나가는 SQL : select m*, t* from Member m inner join Team t on m.team_id = t.team_id;
> 즉시 로딩과 똑같음.
즉시 로딩을, 내가 원하는 시점에 정할 수 있는 것!!
// Fetch Join 이해 예시
// 1. Many To One Fetch Join
멤버와 Team 에 대한 연관 관계가 있고, memberA - teamA // memberB - teamB // memberC - teamB 인 상태라고 가정해보고, 다음과 같은 예시를 살펴보자.
@Test
@DisplayName("Fetch Join Example")
void test1() {
// ...
try {
// ...
JpqlMember memberA = new JpqlMember();
memberA.setUsername("회원A");
memberA.setTeam(teamA);
em.persist(memberA);
// ...
em.flush();
em.clear();
String query = "select m from JpqlMember m";
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class).getResultList();
for (JpqlMember member : resultList) {
System.out.println("member = " + member + ", team.getName() = " + member.getTeam().getName()); // 프록시로 가지고 있다가, 이 때 Query 가 나감.
}
tx.commit();
// ...
}
현재 Member에 Team 은 FetchType Lazy 로 설정되어 있기 때문에, resultList 안에 있는 JpqlMember 들은 모두 Team 객체를 프록시 객체로 가지고 있다. (DB에 저장되어 있는 ID만을 가지고 있음)
이후 member.getTeam().getName() 수행시 SQL이 나가게 되므로, 출력결과는 다음과 같다.
.......
Hibernate : select (team 정보) from JpqlTeam team where team.team_id = ? // 팀A의 호출로 인한 객체 주입 준비
member = JpqlMember{id=3, username='회원A', age=0}, team.getName() = 팀A
......
Hibernate : select (team 정보) from JpqlTeam team where team.team_id = ? // 팀B의 호출로 인한 객체 주입 준비
member = JpqlMember{id=4, username='회원C', age=0}, team.getName() = 팀B
member = JpqlMember{id=5, username='회원C', age=0}, team.getName() = 팀B // 팀 B가 1차 캐시에 있으므로 SQL을 내보내지 않는다.
> SQL 겁나 많이 나가는게 보임.
> 이렇게 되면, 회원 100명에 대한 게시판을 조회 함. N+1 발생.
> N+1 을 막기 위해선.. 사실 Fetch Join 밖에 없음.
다음과 같이 바꿔보자.
...
// FetchJoin 적용
String fetchQuery = "select m from JpqlMember m join fetch m.team";
List<JpqlMember> resultList = em.createQuery(fetchQuery, JpqlMember.class).getResultList();
...
이렇게 되면, 다음과 같이 SQL이 날라가는 것을 알 수 있다.
Hibernate: select
jpqlmember0_.member_id as member_i1_9_0_,
jpqlteam1_.team_id as team_id1_12_1_,
jpqlmember0_.age as age2_9_0_,
jpqlmember0_.team_id as team_id4_9_0_,
jpqlmember0_.username as username3_9_0_,
jpqlteam1_.name as name2_12_1_
from
JpqlMember jpqlmember0_
inner join
JpqlTeam jpqlteam1_
on
jpqlmember0_.team_id=jpqlteam1_.team_id
...
member = JpqlMember{id=3, username='회원A', age=0}, team.getName() = 팀A
member = JpqlMember{id=4, username='회원B', age=0}, team.getName() = 팀B
member = JpqlMember{id=5, username='회원C', age=0}, team.getName() = 팀B
> 쿼리가 한방에 나가고, member 객체들의 team 정보에 대해 한번에 조회되는 것을 알 수 있다.
> em.createQuery 가 나간 시점에 프록시를 가져오는게 아니라, 진짜 JPQLTEAM 객체가 채워져 있는 것임. (SQL 안에 TEAM NAME 까지 넣고 있는 것을 볼 수 있음)
> 조회성에서 많이 사용함.
> 지연로딩으로 Setting 을 했어도, fetch join 명령이 우선임.
// 2. Collection Fetch Join 와 DISTINCT 옵션 (중요)
자 이제 그러면 반대를 해보자. Team 의 입장에서 Member 들을 가지고 오고 싶은 상황임.
...
String query = "select t from JpqlTeam t join fetch t.members"; // 명시적 join 으로 다 들고와보림
List<JpqlTeam> resultList = em.createQuery(query, JpqlTeam.class).getResultList();
for (JpqlTeam team : resultList) {
System.out.println("team.getName() = " + team.getName() + "| member size = " + team.getMembers().size());
}
...
그러면 당연히 다음과 같은 SQL이 나가고 출력될 것이다.
select *
from
jpqlteam t
join
jpqlmember m
on
t.team_id = m.team_id
...
team.getName() = 팀A | member size = 1
team.getName() = 팀B | member size = 2
team.getName() = 팀B | member size = 2
여기서 중요한 것은, Team 을 중심으로 호출을 했는데 중복되는 Team 이 한번 등장한다는 것이다. 바로 MemberB, 와 MemberC 때문. 이걸 조심해야함.
16분 그림
DB 상 1:N 조인을 하면 데이터가 뻥튀기 됨. 그냥 DB에서는 당연히 N을 다 보여줘야 하기 때문에 1쪽을 여러번 보여줄 수밖에 없고, 그게 그냥 DB의 결과임.
그렇게 되면 JPA는 그냥 DB의 결과를 당연히 다 보여줘야 하는 거고, (물론 영컨에서 사용함)
각 팀에 대한 멤버를 모두 호출하면, 아래 두 개는 그냥 똑같은 것만 출력하게 된다. 팀B에 대해, 회원B, 회원C 이렇게 두번 출력함. > JPA 입장에서 이걸 출력하냐 마냐는 사용자가 설정해주도록 맡기는 것임. 자기는 당연히 출력해야함.
> 중복이 싫으면 DISTINCT 를 사용하면 됨.
> SQL의 DISTINCT 뿐만 아니라, 앱에서 엔티티 중복을 제거해줌. (결과가 앱에 올라왔을 경우, 똑같은 엔티티가 있으면 없애준다)
다음과 같이 사용해보자.
...
String query = "select distinct t from JpqlTeam t join fetch t.members"; // DISTINCT 사용
List<JpqlTeam> resultList = em.createQuery(query, JpqlTeam.class).getResultList();
for (JpqlTeam team : resultList) {
System.out.println("team.getName() = " + team.getName() + "| member size = " + team.getMembers().size());
}
...
참고로 SQL에서의 DISTINCT 는 완벽히 똑같아야 함. 지금 출력되는 테이블을 보면
TEAM_ID TEAM_NAME MEMBER_ID TEAM_ID(FK) MEMBER_NAME
1 팀A 3 1 회원A
2 팀B 4 2 회원B
2 팀B 5 2 회원C
이렇게 생겼음. 여기서, TEAM입장에선 같은 녀석들이라 해도, Member_id, Member_name 모두 다르기 때문에, 출력시키는 테이블에서 Distinct 를 걸어도 저거 제거가 안됨.
SQL입장에선 디스팅트 안걸림. (출력시 JPA 가 들어가기 전 SQL 날라가는것까진 해결이 안된 상태라고 보면 됨)
그래서 저렇게 날렸으면 (따로 변경할 건 없음, JPA에서 무슨일을 해주는지가 관건) distinct 가 걸려있을시 Data를 다 읽어오고 앱 단에서 같은 식별자를 가진 Team 엔티티들을 제거해줍니다. (즉, 1차 캐시처럼, Team id가 동일하면 제거해줍니다) >> Member 가 어떤지는 신경 안씀. (즉, DB단에서는 똑같은거임! JPA 앱에서 해주는거임!)
그래서 결과적으로, 출력값을 보면, 마지막 중복이 제거되어 다음과 같이 출력되는 것을 볼 수 있다.
...
team.getName() = 팀A | member size = 1
team.getName() = 팀B | member size = 2
...
for 문 안에 for 문 넣어서 memeber들 (각 팀의 ) 을 출력해보면서 해보면 조금 더 이해가 쉬울 것임.
====================================
정리해보자면, Fetch Join 과 일반 Join 의 차이를 조금 이해해볼 필요가 있음.
지금 똑같은 상황에서 다음 두 쿼리를 비교해보자.
// 1.
String query1 = "select t from JpqlTeam t join t.members";
// 2.
String query2 = "select t from JpqlTeam t join fetch t.members";
// 1 의 출력값. (일반 조인으로 할 시에는 그 객체 정보만을 가져오고, join 만 해줌)
SQL >
select (t의 모든 정보) from Team t
inner join Member m
on t.team_id = m.team_id
>> 일반 Join 은 조인에 대해서만 끼워주는거고, 올려주는 데이터는 Team 에 대한 데이터, 즉 앞에서 select 한 데이터들만 가져와 주는 것임 (JPQL 은 결과를 반환할 때 연관관계 ㅈ도 신경 안씀. 그냥 select 한 거만 올려주는 거임)
>> 따라서 Members 도 ID들만 있지만 (그렇다고 프록시는 또 아님), 로딩 시점에 또 쿼리 ㅈㄴ 날려서 멤버들 다 가져올 것임. (N+1 과 똑가틍ㅁ)
>> (보통 where 절을 사용해서 join 된 id 를 가지고 더 쿼리를 구성함)
// 2의 출력 값
select (m의 모든 정보, t의 모든 정보) from Team t
inner join Member m
on t.team_id = m.team_id
지연로딩 같은 것 없이, 한번에 다 가져옴. m,t 의 모든 정보
정리
>> 일반 조인 실행시 연관 엔티티를 함께 조회하지 않는다
>> Fetch Join 을 사용하는건 그 객체의 페치 타입과 관계 없이 즉시 로딩을 하는 것임. 즉, 객체 그래프 SQL을 한꺼번에 조회하는 개념!
>> 항상 Lazy Loading 으로 깔되, 최적화가 필요한 곳이 발견되거나 N+1 이 발견될 시 Fetch Join 을 적용한다.
>> N+1 은 그냥 이걸로 다 해결한다. (JPA 성능 문제의 대부분은 Fetch Join 으로 잡을 수 있었음)
>> 뭐 이게 전설이다!!! 이러는건 절대 아님. 사용하다보면 Entity 가 아닌, 전혀 다른 이거와 저거를 합친 모델을 가져와야 하는 경우가 생각보다 많음. 이럴 경우에는 페치 조인을 쓰기보단 DTO 로 한 데이터 묶음을 만들어서 반환하는 것이 제일 효과적이다. (실무 예시:: 하기)
@Override
public List<QueryManagingMemberDto> findMemberByManagingPartId(Long partId){
/*
Query:
select (~~) from member m
join part_manager_member_linker pmm
on m.member_id = pmm.member_id
where pmm.part_id = {part_id}
*/
// 엔티티 모델대로가 아닌, 전혀 다른 형식의 데이터가 필요할 때 DTO로 추출
return queryFactory.select(Projections.constructor(QueryManagingMemberDto.class
,member.id, member.username, member.knoxId, partManagerMemberLinker.managerType))
.from(member)
.join(partManagerMemberLinker)
.on(member.id.eq(partManagerMemberLinker.member.id))
.where(partManagerMemberLinker.part.id.eq(partId))
.fetch();
}
****** Fetch Join 의 특징과 한계
1) 패치 조인 대상에는 별칭을 줄 수 없음 (객체 그래프를 다 조회했는데 일부만 사용하는 것은 굉장히 위험한 행동)
String query = "select t From JpqlTeam t join fetch t.members as m";
이 마지막 'as m' 가지고 막 wherer m. ~~ 이런걸 하면 안됨. 하고 싶으면 그냥 따로 좀 조회해라... 굳이 멀 패치조인하는데 ... 별칭을 주지 않고 따로 사용하지 않는 것이 관례. >> 보통 그래야 할 것 같은 경우, 반드시 다른 방법이 있음.
아니 뭐 다 불러오는데 나이 10 살 이상인 이런거 하고 싶음.
그러면 ..... Member 에서 조회를 한 다음에 짤라서 쓰고 team 을 join 해야지 이런식으로 가면 안되는 거임.
2) 둘 이상의 컬렉션은 패치 조인 할 수 없다.
> 데이터 정합성에 문제가 발생할 수 있음.
> 잘못하면 데이터가 엄청 많아지는데, 이상이 생길 가능성이 크기 때문. (이 정도로 cross 하면 데이터가 잘 안맞기도 함)
3) Collection Fetch Join 에서는 Paging 을 사용할 수 없다. (매우 아쉽)
> 일대일, 다대일 같은 단일 값 연관 필드들은 패치 조인을 해도 페이징 사용 가능 (데이터 뻥튀기가 없기 때문)
> Collection Fetch Join 을 해야 하는 경우에는 Paging 사용 불가 (아까 했던 예시를 다시 살펴보자, 매우 위험)
예를 들어,
Team A 에 속한 회원이 두명 있다고 가정해보자.
이 때 Collection Fetch 를 했을 경우, 테이블이 팀A 가 중복되고 MemberA, MemberB 에 대한 데이터를 보여주기 위해 데이터가 뻥튀기 된다 (위에서 봤던 현상, 굉장히 자연스러운 DB의 모습) > 이 때 데이터들의 정합성? 단위? 가 보여주는 것이 다르기 때문에, paging 을 하면 자칫하면 팀A에는 멤버가 한명밖에 없네! 이런식으로 해석될 수도 있다는 것임.
> 데이터의 의미가 달라짐.
> Hibernate 에서는 경고 로그를 남기고 메모리에서 페이징 처리를 한다.
> 이것도 항상 다른 방식이 있음 ... 다대일로 뒤집거나..... 굳이 안된다는데 해야겠다 ㅅㅂ! 할 필요가 없음...
==================== 페이징 설정 관련해서 더 듣고 싶으면 패치조인 2 - 한계 강의 12분대 부터 다시 듣기 =======
'Spring > JPA' 카테고리의 다른 글
[JPA] 연관 객체 불러오기.. Fetch Join 정말 괜찮을까? (0) | 2023.12.03 |
---|---|
[JPA] - 나머지 쿼리 (0) | 2023.01.04 |
[JPA] JPQL 쿼리 (0) | 2022.12.29 |
[JPA] - 값 타입 (0) | 2022.12.29 |
[JPA] 프록시와 연관관계 - 2 (0) | 2022.12.28 |