Language`/JPA

[QueryDSL] Join & SubQuery

Avenus 2022. 8. 28. 12:45

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이다

왼쪽 = 원래 쿼리 / 오른쪽 = Team Proxy에 대해서 조회하기 위한 추가 쿼리

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절 서브쿼리는 지원하지 않는다