스프링데이터 + JPA/QueryDSL

17. 서브 쿼리

sdafdq 2023. 11. 30. 11:25

그 왜 SQL에서 막 쓰다가 () 괄호 하고 새 쿼리 쓰는 그걸 서브 쿼리라고 함.

일단 서브쿼리를 사용할 때는 JPAExpressions, 직역은 JPA 표현식을 이용

//    나이가 가장 많은 회원 조회
@Test
public void subQuery(){
    QMember memberSub = new QMember("memberSub");
    Member member = query.selectFrom(m).orderBy(m.age.desc()).limit(1).fetchOne();
    System.out.println("member = " + member.getAge());

    List<Member> result = query.selectFrom(m).
            where(
                    m.age.eq(
                            JPAExpressions.
                                    select(memberSub.age.max()).from(memberSub)
                    )
            ).fetch();

    assertThat(result.get(0).getAge()).isEqualTo(40);
}

사실 저 member 처럼 그냥 저렇게 해도 되긴 하는데, 서브쿼리 연습용이라,

보면 다 똑같은데, 서브쿼리 하고 싶은 부분만 JPAExpressions.하면서 시작. 똑같음. 저것도 쿼리 팩토리라고 생각하면 됨.

 

일단 멤버를 조회해 오는데, 멤버의 나이가 서브쿼리해서 얻은 멤버의 최고 나이랑 같은거.

 

지금 멤버를 @BeforeEach로 10,20,30,40 해서 넣어둠.

 

지금 보면 서브쿼리용 별칭을 따로 생성해서 사용함. 사실 그냥 m 써도 결과는 같음.

근데 이렇게 하는 이유는, 지금 쿼리메소드들 같은 경우에는 충돌 날 여지가 없지만 혹시라도 충돌이 날 수 있고, 

또 서브쿼리는 이렇게 명확하게 독립적으로 별칭을 사용해서 메인 쿼리와 나누는 게 유지보수 면에서도 명확하게 알 수 있음.

 

여튼 저렇게 서브쿼리를 날리면, max() 함수를 DB에 요청해서 member중 제일 나이가 많은 숫자를 가지고 오게 될거임.

그거를 m.age = 

where m.age = (서브쿼리)

 

나가는 jpql을 예상해 보자면

select m from Member m where m.age = (select max(memberSub.age) from Member memberSub )

이럴 까 함.

 

가장 많은 게 40이라 테스트는 통과.

/* select
    member1 
from
    Member member1 
where
    member1.age = (
        select
            max(memberSub.age) 
        from
            Member memberSub
    ) */ select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.age=(
            select
                max(m2_0.age) 
            from
                member m2_0
        )

같음.

 

@Test
public void subQueryGoe(){
    QMember memberSub = new QMember("memberSub");
    List<Member> result = query.selectFrom(m).where(m.age.goe(JPAExpressions.select(memberSub.age.avg()).from(memberSub))).fetch();
    assertThat(result).extracting("age").containsExactly(30,40);
}

나이 평균 이상만.

위랑 비슷한데 조건만 약간 달라짐.

member를 얻어오는데 where하고 조건이 멤버의 나이가 크거나 같은 거

멤버의 평균보다. 

select m from Member m where m.age >= (select avg(memberSub.age) from Member memberSub )

/* select
    member1 
from
    Member member1 
where
    member1.age >= (
        select
            avg(memberSub.age) 
        from
            Member memberSub
    ) */ select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.age>=(
            select
                avg(cast(m2_0.age as float(53))) 
            from
                member m2_0
        )

같음.

 

이제 약간 다름.

//   멤버 중 10살 초과인 멤버들만 출력
@Test
public void subQueryIn(){
    QMember memberSub = new QMember("memberSub");

    List<Member> result = query.
            selectFrom(m).where(
                    m.age.in(
                            JPAExpressions.
                                    select(memberSub.age).from(memberSub).where(memberSub.age.gt(10))
                    )
            ).fetch();
    assertThat(result).extracting("age").containsExactly(20,30,40);
}

이번엔 서브쿼리로 여러개의 row를 가져올 것이고, 그걸 in의 조건으로 만듦.

이것도 사실 select m from Member m where m.age > 10 하면 되긴 하는데..

 

일단 쿼리메소드 분석을 해 보자면

멤버에서 멤버들을 가져오는데,

조건이

멤버의 나이가

in

멤버의 나이들 중 10 초과인 멤버들의 나이를 row들로 여러 개 가져옴. 

 

jpql로 바꿔보면

select m from Member m where m.age in (select memberSub.age from Member memberSub where memberSub.age > 10)

/* select
    member1 
from
    Member member1 
where
    member1.age in (select
        memberSub.age 
    from
        Member memberSub 
    where
        memberSub.age > ?1) */ select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.age in (select
            m2_0.age 
        from
            member m2_0 
        where
            m2_0.age>?)

같음.

 

@Test
public void selectSubQuery(){
    QMember memberSub = new QMember("memberSub");

    List<Tuple> result = query.select(m.username,
            JPAExpressions.select(memberSub.age.avg()).from(memberSub)
    ).from(m).fetch();

    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
}

이거는 select 절에서 서브쿼리로 평균과 같이 가져오는 거.

 

멤버의 이름과, 서브쿼리로 멤버들 나이의 평균을 같이 가져옴.

 

jpql

select m.username, (select avg(memberSub.age) from Member memberSub) from Member m

/* select
    member1.username,
    (select
        avg(memberSub.age) 
    from
        Member memberSub) 
from
    Member member1 */ select
        m1_0.username,
        (select
            avg(cast(m2_0.age as float(53))) 
        from
            member m2_0) 
    from
        member m1_0

같음.

 

출력 결과는

tuple = [member1, 25.0]
tuple = [member2, 25.0]
tuple = [member3, 25.0]
tuple = [member4, 25.0]

의도했던 대로 멤버의 이름과, 나이 평균이 나옴.

 

참고로, JPAExpressions는 정적 import가 가능함.

 

 

지금까지 보면, where 절과 select 절에만 사용을 했음.

where절이 가능하다면 비슷하게 on절도 아마 가능할꺼임.

 

근데 아직 from 절에는 '공식적인 지원'은 안함.

6.1부터 추가되기는 했으나, 아직 이슈가 있어 공식적인 지원은 안함.

 

jpql 자체에서 from절에 서브쿼리를 지원하지 않는다.

 

원래 JPA에서는 select 절 서브쿼리도 안된다고 한다. 그러니까, 표준 스펙은 아니라고 한다.

그런데 우리는 그 JPA 인터페이스를 구현한 구현체인 하이버네이트를 사용하고 있는데, 하이버네이트에서는 select절의 서브쿼리를 지원한다. QueryDSL도 하이버네이트를 사용.

 

 

그래서, 여튼 공식적으로는 from절에 서브쿼리를 지원하지 않기 때문에, 해결방법은

서브쿼리는 보통 join으로 변경이 왠만하면 가능하다. (불가능한 상황도 있긴 하다.)

그 다음, 위에 보다는 비효율적이지만, 쿼리를 2번 나눠서 실행.

그래도 안된다면 결국 네이티브 쿼리

 

'스프링데이터 + JPA > QueryDSL' 카테고리의 다른 글

19. 상수, 문자 더하기  (0) 2023.11.30
18. CASE 문  (0) 2023.11.30
16. join fetch  (0) 2023.11.29
15. join on  (0) 2023.11.29
14. join  (0) 2023.11.29