BackEnd/JPA

[JPA] QueryDSL 기본문법 - 4

샤아이인 2022. 5. 14.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

 

11. 서브 쿼리

서브쿼리를 만들기 위해서는 com.querydsl.jpa.JPAExpressions 을 사용하면 된다.

JPAExpressions 또한 static import를 하여 편리하게 사용할 수 있다.

11-1) SubQuery의 eq

다음과 같이 나이가 가장 많은 회원을 찾는다고 해보자.

/**
 * 나이가 가장 많은 회원 조회
 */
@Test
public void subQuery_test() {

    Member member10 = new Member("member10", 40);
    em.persist(member10);

    QMember memberSub = new QMember("memberSub");

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

    assertThat(result).extracting("age").containsExactly(40);
}

테스트는 정상적으로 통과된다.

 

실행되는 쿼리를 보면 다음과 같다.

select
    member0_.member_id as member_i1_1_,
    member0_.age as age2_1_,
    member0_.team_id as team_id4_1_,
    member0_.username as username3_1_ 
from
    member member0_ 
where
    member0_.age=(
        select
            max(member1_.age) 
        from
            member member1_
    )

 

11-2) SubQuery의 goe

@DisplayName("나이가 평균 이상인 회원 조회")
@Test
public void subQuery_age_avg_goe_test() {

    Member member10 = new Member("member10", 40);
    em.persist(member10);

    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.goe(
                    JPAExpressions
                            .select(memberSub.age.avg())
                            .from(memberSub)
            )).fetch();

    assertThat(result).extracting("age").containsExactly(40);
}

 

11-3) SubQuery에서의 in절 사용하기

@DisplayName("서브 쿼리에서 in절 사용")
@Test
public void subQuery_in_test() {

    Member member10 = new Member("member10", 40);
    em.persist(member10);

    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    JPAExpressions
                            .select(memberSub.age)
                            .from(memberSub)
                            .where(memberSub.age.gt(10))
            )).fetch();

    assertThat(result).extracting("age").containsExactly(20, 20, 20, 40);
}

 

11-4) Select 절 에서의 SubQuery

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

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

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

select 절의 2번째 인자로 subQuery를 전달하고 있다.

 

실행 결과는 다음과 같다.

 

11-5) From 절 에서의 SubQuery

결론부터 말하면 From 절에서의 SubQuery는 지원하지 않는다.

 

JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.

당연히 JPQL의 빌더역할을 하는 Querydsl 또한 이를 지원하지 않는다.

 

원래 JPA에서는 select에서의 subquery또한 지원하지 않지만, Hibernate 구현체를 사용하면 select 절의 서브쿼리는 지원한다.

따라서 Querydsl도 Hibernate 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

 

▶ from 절의 서브쿼리 해결방안

  1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
  2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  3. nativeSQL을 사용한다.

 

12. Case문

12-1) 단순 조건 case

@Test
public void basic_case_test() {
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("10살")
                    .when(20).then("20살")
                    .otherwise("성인 입니다"))
            .from(member)
            .fetch();

    for (String s : result) {
        System.out.println("s = " + s);
    }
}

실행되는 쿼리는 다음과 같다.

select
    case 
        when member0_.age=? then ? 
        when member0_.age=? then ? 
        else '성인 입니다' 
    end as col_0_0_ 
from
    member member0_

결과는 다음과 같다.

 

12-2) 복합한 조건 case

@Test
public void complex_case_test() {
    Member member10 = new Member("member10", 40);
    em.persist(member10);

    List<String> result = queryFactory
            .select(new CaseBuilder()
                    .when(member.age.between(0, 20)).then("0~20살")
                    .when(member.age.between(21, 40)).then("21~40살")
                    .otherwise("기타"))
            .from(member)
            .fetch();

    for (String s : result) {
        System.out.println("s = " + s);
    }
}

실행 결과는 다음과 같다.

실행된 SQL은 다음과 같다.

select
    case 
        when member0_.age between ? and ? then ? 
        when member0_.age between ? and ? then ? 
        else '기타' 
    end as col_0_0_ 
from
    member member0_

 

하지만 이러한 기능을 정말 사용해야 할까?

DB에서는 최소한의 grouping과 filtering을 통해 데이터를 줄이는 일만 하고, 실제 case별 상황이나 화면에 보여주는 일은 DB에서 하면 안된다.

 

나이가 10이면 10살, 20이면 20살의 표현은 에플리케이션 level에서 처리하기를 권장한다.

 

13. 상수, 문자 더하기

13-1) 상수 사용하기

@Test
public void constant() {
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();

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

상수가 필요하면 Expressions.constant(xxx)을 위와같이 사용하면 된다.

 

13-2) 문자 더하기 concat

@Test
public void constant() {
    String result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("member1"))
            .fetchOne();

    System.out.println("result = " + result);
}

실행하면 결과로 "member1_10"이 나와야 한다. 이게 정상이다!

다만 H2 db로 인해 일부분이 잘려 "member1_1"로 출력되게 된다.

왜 이렇게 나오는 것 일까?

 

우선 실행된 SQL문은 다음과 같다.

select
    ((member0_.username||?)||cast(member0_.age as char)) as col_0_0_ 
from
    member member0_ 
where
    member0_.username=?

select 절에 보면 cast(member0_.age as char) 로 casting이 되는것을 확인할 수 있다.

H2 2.0.202에서는 change log를 확인해보니 char타입 기본 길이가 1로 고정되어 cast(10 as char)가 1만 반환되는 것 이다.

 

해당 issue

https://github.com/h2database/h2database/issues/2266

 

CHAR and BINARY should have length 1 by default · Issue #2266 · h2database/h2database

The SQL Standard requires length of 1 by default for CHAR, CHARACTER, and BINARY data types: <character string type> ::= CHARACTER [ <left paren> <character length> <right pare...

github.com

 

'BackEnd > JPA' 카테고리의 다른 글

[JPA] QueryDSL 중급문법 - 2  (0) 2022.05.14
[JPA] QueryDSL 중급문법 - 1  (0) 2022.05.14
[JPA] QueryDSL 기본문법 - 3  (0) 2022.05.14
[JPA] QueryDSL 기본문법 - 2  (0) 2022.05.13
[JPA] QueryDSL 기본문법 - 1  (0) 2022.05.13

댓글