[QueryDSL] Join & SubQuery
QueryDSL Join
QueryDSL에서는 JPQL에서 지원하는 [InnerJoin, LeftOuterJoin]은 당연하게 사용할 수 있고 더해서 [RightOuterJoin]도 사용할 수 있다
추가적으로 on절과 더불어서 성능 최적화를 위한 FetchJoin도 활용할 수 있다
InnerJoin
첫번째 파라미터(Root Entity에 대한 조인 대상)
두번째 파라미터(조인 대상의 Alias)
List<Member> fetch = query.selectFrom(member)
.innerJoin(member.team, team)
.where(member.age.goe(25).and(team.name.eq("Team-A")))
.fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam()); // Team Proxy에 대한 초기화 과정 수행
}
현재 Member:Team은 1:N관계이고 위의 query를 통해서 [나이 ≥ 25 && 팀이름 = "Team-A"]인 멤버를 찾으려고 한다
그렇게 찾은 Member에 대해서 Member의 Team정보도 확인해보려고 하는데 여기서 예상할 수 있는 가장 대표적인 문제는 N+1이다
Member를 가져올때 Member의 Team은 Proxy로 가져왔기 때문에 당연히 Team에 대해서 접근할 때 프록시 초기화 과정이 이루어져서 Team에 대해서 실제로 DB에서 조회하게 된다
>> 이러한 N+1문제를 해결하기 위해서 fetchJoin을 활용해야 한다
List<Member> fetch = query.selectFrom(member)
.innerJoin(member.team, team).fetchJoin()
.where(member.age.goe(25).and(team.name.eq("Team-A")))
.fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam());
}
QueryDSL에서 fetchJoin을 사용하기 위해서는 그냥 xxxJoin()에 fetchJoin()을 chaining시켜주면 된다
fetchJoin덕분에 Member와 LAZY로 매핑된 Team도 즉시 가져옴을 확인할 수 있다
ThetaJoin(CrossJoin)
innerJoin()이나 left/right/fullJoin()없이 from절에 여러 엔티티를 작성하게 되면 해당 엔티티끼리 "카티션 곱"이 발생하는 CrossJoin을 수행하게 된다
List<Tuple> fetch = query.select(member, team)
.from(member, team)
.fetch();
멤버 13명 × 팀 5명의 모든 경우의 수인 65가지가 결과로 출력됨을 확인할 수 있다
leftJoin (Left Outer Join)
List<Member> fetch = query.selectFrom(member)
.leftJoin(member.team, team)
.fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam()); // Team Proxy에 대한 초기화 과정 수행
}
Team이 없는 Member도 조회하기 위해서 Member - Team간에 Left Outer Join을 구현한 쿼리이다
마찬가지로 Member를 가져올때 Member와 연관된 Team은 LAZY Loading전략을 따르기 때문에 "Team의 프록시 객체"로 가져오게 된다
그리고 실질적으로 "member.getTeam()"에 의해서 Team에 직접적으로 접근할 때 "Team 프록시에 대한 초기화 과정"이 이루어지면서 Team에 대한 Query가 추가적으로 나가게 된다
>> Left Outer Join에도 fetchJoin()을 적용해보자
List<Member> fetch = query.selectFrom(member)
.leftJoin(member.team, team).fetchJoin()
.fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam());
}
rightJoin (Right Outer Join)
QueryDSL에서는 JPQL에서는 지원하지 않는 [Right Outer Join]까지 지원해준다
이번에는 Team을 기준으로 Team에 소속된 멤버들을 구하는 Member - Team ==> Right Outer Join
List<Member> fetch = query.selectFrom(member)
.rightJoin(member.team, team).fetchJoin()
.orderBy(member.id.asc())
.fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam());
}
- Team이 없는 [2, 10, 12]Member들은 쿼리에 포함되지 않음을 확인할 수 있다
QueryDSL SubQuery
QueryDSL에서 SubQuery를 사용하는 방식은 2가지로 나눌 수 있다
1. new JPASubQuery(~~~)로 서브쿼리를 생성
2. JPAExpressions를 static import하고 편리하게 select(~~~)로 서브쿼리 생성
어차피 JPAExpressions도 내부적으로 서브쿼리를 생성할 때 "new JPASubQuery"를 통해서 생성하기 때문에 JPAExpressions를 static import하고 편리하게 쓰는것이 생산성에서 더 좋아보인다
1. Where절 서브쿼리 (age에 대한 equal SubQuery)
최연소 멤버 & 최고령 멤버
// 최연소 멤버
List<Member> fetch = query.selectFrom(member)
.innerJoin(member.team, team).fetchJoin()
.where(member.age.eq(
select(member.age.min()) // Member중에서 최연소 Member
.from(member)
)).fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam());
}
// 최고령 멤버
List<Member> fetch = query.selectFrom(member)
.innerJoin(member.team, team).fetchJoin()
.where(member.age.eq(
select(member.age.max()()) // Member중에서 최고령 Member
.from(member)
)).fetch();
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam());
}
2) Where절 SubQuery (age에 대한 IN SubQuery)
평균 나이 이상인 멤버들
List<Member> fetch = query.selectFrom(member)
.innerJoin(member.team, team).fetchJoin()
.where(member.age.in(
select(member.age)
.from(member)
.where(member.age.between(
select(member.age.avg().intValue()).from(member),
select(member.age.max()).from(member)
)
)
)).fetch();
Integer avgAge = query.select(member.age.avg().intValue())
.from(member)
.fetchOne();
System.out.println("평균 나이 = " + avgAge);
for (Member member : fetch) {
System.out.println("Member = " + member);
System.out.println("\tTeam = " + member.getTeam());
}
From절 서브쿼리는 JPA의 구현체인 Hibernate를 사용하면 여전히 활용할 수 없는 서브쿼리이다
당연히 QueryDSL도 From절 서브쿼리는 지원하지 않는다