gony-dev 님의 블로그

[Querydsl] 기본 문법 본문

Querydsl

[Querydsl] 기본 문법

minarinamu 2024. 11. 25. 18:05

 

Querydsl에 대한 초기 설정을 하였지만
기초도 모르면 무용지물이다. 오늘은 querydsl을 사용하여 기본 문법에 대해 작성을 해보겠다.

JPQL vs. Querydsl

  • 작성 전에 우리가 querydsl을 사용하는 이유에 대해 알아보자.
    JPQL을 사용해도 될텐데 왜 굳이 querydsl을 사용하는 것일까?
    사실 이 두 가지 프레임워크에는 차이점이 있다.
  • JPQL과 달리 querydsl은 select절에서 문법이 틀려도 틀릴수가 없다.
    왜? 컴파일링을 통해 오류를 다 잡아내기 때문이다!! 그 덕에 우리는 편하게 코드를 작성하고 재빨리 오류를 찾아 수정할 수 있다.
  • "여러 Repository에서 JPAQueryFactory를 필드로 선언할 텐데 이에 대한 동시성 문제는 괜찮을까?" 라고 생각할 수 있다.
    하지만 괜찮다. 사전에 여러 멀티스레드에서 접근을 해도 이전에 문제없게 설계가 되어있어 문제가 되지 않는다!

기본적인 Q-Type 활용

쿼리에 사용되는 Type은 이 QClass를 사용하는데, 이 클래스를 선언하는 방법에는 3가지가 있다.

  1. 별칭 생성 및 선언
    ex. QMember m = new QMember("m");
  2. 바로 선언
    ex. QMember m = QMember.member;
  3. static import 하기

취향껏 선언하면 될 것 같다 :)


검색 조건 쿼리

검색 조건 쿼리는 where 절로 원하는 결과를 조회하고 싶을 때 사용한다.

@DisplayName("검색 조건을 활용하여 원하는 객체를 조회한다.")
@Test
void search(){
    //given
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1").and(member.age.eq(10)))
            .fetchOne();

    // then
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

@DisplayName("체인을 사용하여 검색 조건을 추가할 수 있다.")
@Test
void searchAndParam(){
    //given
    Member findMember = queryFactory
            .selectFrom(member)
            .where(
                    member.username.eq("member1"),
                    member.age.eq(10)
            )
            .fetchOne();

    // then
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

이 외에도 JPQL이 제공하는 검색 기능은 다음과 같다.

1. equals
    - eq: 동등하다.
    - ne: 동등하지 않다.
    - eq(xxx).not(): 동등하지 않다.
2. null
    - isNotNull(): 이름이 is not null
3. in
    - in(a, b): a나 b 값을 가진
    - notIn(a, b): a나 b 값을 가지지 않은
    - between(a, b): a와 b 사이의 값을 가진
4. size 비교
    - goe(x): x보다 크거나 같은
    - gt(x): x보다 큰
    - loe(x): x보다 작거나 같은
    - lt(x): x보다 작은

5. 문자열 검색
    - like("x%"): x와 하나의 문자를 가진
    - contains("x"): x를 포함하는
    - startsWith("x"): x로 시작하는

결과 조회

사용자는 결과를 하나, 여러 개, 또는 본인이 원하는 만큼 조회할 수 있다.

  1. fetch()
    • 리스트 조회, 데이터 없으면 빈 리스트 반환
  2. fetchOne()
    • 하나의 결과 조회
    • 결과가 없으면 null이고, 둘 이상이면 NonUniqueResultException을 발행
  3. fetchFirst()
    • limit(1).fetchOne()
  4. fetchResults()
    • 페이징 정보를 포함하고, total count 쿼리를 추가 실행한다.
  5. fetchCount()
    • count 쿼리로 변경해서 count 수를 조회한다.
@DisplayName("여러가지 결과 조회를 테스트해본다.")
@Test
void resultFetch(){
 //given
    List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();

    Member fetchOne = queryFactory
            .selectFrom(member)
            .fetchOne();

    Member fetchFirst = queryFactory
            .selectFrom(member)
            .fetchFirst();// ==limit(1).fetchOne();

    QueryResults<Member> results = queryFactory
            .selectFrom(member)
            .fetchResults();

    results.getTotal(); // total count를 가지고 와야하므로 select를 두 번 실행한다.
    List<Member> content = results.getResults();

    long total = queryFactory
            .selectFrom(member)
            .fetchCount();

}

정렬

사용자는 오름차순 or 내림차순으로 정렬할 수 있다.

아래의 코드를 돌려보면 회원의 나이를 내림차순, 동일한 값에 대하여 닉네임을 오름차순하는 것을 알 수 있다.

    @DisplayName("회원들을 특정 속성을 베이스로 정렬한다.")
    @Test
    void sort(){
     //given
        em.persist(new Member(null, 100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));

        //when
        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();
        Member member5 = result.get(0);
        Member member6 = result.get(1);
        Member memberNull = result.get(2);

     //then
        assertThat(member5.getUsername()).isEqualTo("member5");
        assertThat(member6.getUsername()).isEqualTo("member6");
        assertThat(memberNull.getUsername()).isNull();
    }

페이징

JPA에서도 구현하였던 페이징 기술을 querydsl을 통해서도 구현할 수 있다!!

    @DisplayName("페이징 기술을 구현한다.")
    @Test
    void paging1(){
     //given
        List<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetch();
    }

    @DisplayName("페이징 기술을 구현한다.")
    @Test
    void paging2(){
        QueryResults<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetchResults();

        assertThat(result.getTotal()).isEqualTo(4);
        assertThat(result.getLimit()).isEqualTo(2);
        assertThat(result.getOffset()).isEqualTo(1);
        assertThat(result.getResults().size()).isEqualTo(2);
    }
  • 위의 두 메서드의 다른 점은 paging1은 단순 결과만을 조회하는 반면,
    paging2는 fetchResults()로 인해 count 쿼리를 먼저 실행하고, 엔티티의 내용을 조회한다. 또한 fetchResults()는 페이징 정보를 가지고 있다는 점에서 차이점이 있다.

집합

집합 함수에는 여러 가지가 있다.

  • count(): 데이터 결과 개수를 반환
  • sum(): 데이터 결과의 합을 반환
  • avg(): 데이터 결과의 평균을 반환
  • max(): 데이터 결과들 중 최댓값을 반환
  • min(): 데이터 결과들 중 최솟값을 반환
    @DisplayName("다양한 집합 함수를 이용하여 결과를 집계한다.")
    @Test
    void aggregation(){
        List<com.querydsl.core.Tuple> result = queryFactory
                .select(
                        member.count(),
                        member.age.sum(),
                        member.age.avg(),
                        member.age.max(),
                        member.age.min()
                )
                .from(member)
                .fetch();

        Tuple tuple = result.get(0);
        assertThat(tuple.get(member.count())).isEqualTo(4);
        assertThat(tuple.get(member.age.sum())).isEqualTo(100);
        assertThat(tuple.get(member.age.avg())).isEqualTo(25);
        assertThat(tuple.get(member.age.max())).isEqualTo(40);
        assertThat(tuple.get(member.age.min())).isEqualTo(10);
    }

조인

1. 기본 조인

조인에는 크게 두 가지 종류가 있다.

  • 기본 조인 | 조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고,
    두 번째 파라미터에 별칭으로 사용할 Q타입을 지정하면 된다.
  • 세타 조인 | 세타조인은 from 절에 여러 엔티티를 선택해서 조인할 수 있다.
    이 때 제약 조건이 한 가지 있는데, 외부 조인이 불가능하다는 점이다. 하지만 조인 on절을 사용하면 외부 조인을 사용할 수 있다!

member의 이름이 teamA, B..인 것은 오류가 아니라 조인이 잘 작용하는지 확인하기 위함이므로 오해않기를 바란다.

@DisplayName("팀 A에 소속된 모든 회원을 찾아라!")
@Test
void join(){
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("member1", "member2");
}

@DisplayName("팀 A에 소속된 모든 회원을 찾아라!")
@Test
void theta_join(){
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

    List<Member> result = queryFactory
            .selectFrom(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}

2. on 절

  • 조인 ON 절을 활용할 때에는 두 가지 경우가 있다.
  • 하나는 조인 대상을 필터링을 할 때와 다른 하나는 연관관계 없는 엔티티를 외부 조인할 때이다.
  1. 조인 대상을 필터링
    • on 절을 활용하여 조인 대상을 필터링 할 때는 외부조인이 아니라 내부조인을 사용하면,
      where절에서 필터링하는 것과 기능이 동일하다!
      따라서, on 절을 활용한 조인 대상 필터링을 사용할 때는 내부조인이면 where로 해결하고, 외부조인이 필요한 경우에만
      이 기능을 사용하자!
  2. 연관관계 없는 엔티티를 외부 조인
    • "leftJoin"을 사용 시, 해당 엔티티의 pk가 from 절에 들어있는지 유무에 대해 상관없이 표기하기에 값이 해당되는 값이 없으면 null로 표기된다.
    • 이 경우 문법을 잘 보아야 하며, "leftJoin" 부분에 일반 조인과 다르게 엔티티 하나만 들어간다!
// 조인 대상을 필터링 할 때
@DisplayName("회원과 팀을 조인하면서, 팀 이름인 teamA인 팀만 조인, 회원은 모두 조회한다.")
@Test
void join_on_filtering(){
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team).on(team.name.eq("teamA"))
            .fetch();

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

// 연관관계 없는 엔티티 외부 조인
@DisplayName("회원의 이름이 팀 이름과 같은 회원을 조회한다.")
@Test
void join_on_no_relation() {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

    List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team).on(member.username.eq(team.name))
                .fetch();


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

3. 페치 조인

  • 페치 조인은 SQL에서 제공하는 기능은 아니지만 SQL 조인을 이용하여 연관된 엔티티를 SQL 한번에 조회하는 기능이다.
  • 주로 성능 최적화에 사용되기에 잘 알아두길 바란다!
  • 아래의 코드는 페치 조인이 없을 때와 있을 때의 차이를 구분하기 위해 메서드 2개를 두었다.
@PersistenceUnit
EntityManagerFactory emf;

@DisplayName("페치 조인이 없는 경우의 쿼리를 작성한다.")
@Test
void fetchJoinNo(){
 //given
    em.flush();
    em.clear();
 //when
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"))
            .fetchOne();
    //then
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 미적용").isFalse();
}

// 실무에서 많이 사용!
@DisplayName("페치 조인을 사용하여 쿼리를 작성한다.")
@Test
void fetchJoinUse(){
    //given
    em.flush();
    em.clear();
    //when
    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()
            .where(member.username.eq("member1"))
            .fetchOne();
    //then
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 미적용").isTrue();
}
  • fetchJoinNo()는 loaded에 대해 판별할 때, team에 대한 어떠한 값도 가져오지 않았기에 false가 되고,
    fetchJoinUse()는 fetchJoin을 사용하여 연관된 팀을 끌어오기 때문에 loaded에 대한 member의 팀의 조회가 true가 된다.

서브 쿼리

  • 서브쿼리는 'com.querydsl.jpa.JPAExpressions'를 사용하여 where 안에 쿼리를 넣어 조건에 맞는 데이터를 조회한다.
  • from 절의 서브쿼리에는 한계가 존재하는데 이는 다음과 같다.
    • JPQL의 한계점으로 from 절의 서브쿼리는 지원하지 않는다.
    • Querydsl도 마찬가지로 지원하지 않는다.
    • 하지만 하이버네이트 구현체를 사용하는 경우에는 select 절의 서브쿼리를 지원하며,
      마찬가지로 Querydsl도 하이버네이트 구현체를 사용하여 select 절의 서브쿼리를 지원한다.
  • from 절의 서브쿼리를 해결하려면 3가지 방법이 있다.
    1. 서브쿼리를 join으로 변경한다.
    2. 어플리케이션에서 쿼리를 2번 분리해서 실행한다.
    3. nativeSQL을 사용한다.

실습을 진행해보자.

@DisplayName("나이가 평균 이상인 회원을 조회한다.")
@Test
void subQueryGoe(){
 //given
    QMember memberSub = new QMember("memberSub");
 //when
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.goe(
                    JPAExpressions
                            .select(memberSub.age.avg())
                            .from(memberSub)
            ))
            .fetch();
 //then
    assertThat(result).extracting("age")
            .containsExactly(30, 40);
}

@DisplayName("나이가 평균 이상인 회원을 조회한다.")
@Test
void subQueryIn(){
    //given
    QMember memberSub = new QMember("memberSub");
    //when
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    JPAExpressions
                            .select(memberSub.age)
                            .from(memberSub)
                            .where(memberSub.age.gt(10))
            ))
            .fetch();
    //then
    assertThat(result).extracting("age")
            .containsExactly(20, 30, 40);
}
  • 위와 같이 where에 서브쿼리를 작성하여 원하는 데이터를 조회할 수가 있었다!

CASE 문

  • 자바 기본 문법에서 나오는 if나 case 절에 해당하는 조건식과 같은 맥락으로 작용하며,
    select, where 절에서 사용할 수 있다!
  • 하지만 이때 주의해야 할 점은 데이터를 전환하고 바꾸는 것을 보여주는 것은 DB에서 보여주면 안된다.
    • DB에서 바꾸어 버리는 것은 로그 추적이 어렵고 스펙이 바뀔 수 있으므로, 어플리케이션에서 해결하는 것을 추천한다!
@DisplayName("복잡한 조건의 Case문")
@Test
void complexCase(){
 //when
    List<String> result = queryFactory
            .select(new CaseBuilder()
                    .when(member.age.between(0, 20)).then("0~20살")
                    .when(member.age.between(21, 30)).then("21~30살")
                    .otherwise("기타"))
            .from(member)
            .fetch();
    //then
    for (String s : result) {
        System.out.println("s = " + s);
    }
}
  • 위와 같이 when().then()을 사용하여 조건에 따른 데이터 결과를 변경 및 조회할 수 있다.

상수, 문자 더하기

  • 상수가 필요하면 "Expressions.constant()"를 사용하여 가져온다.
@DisplayName("상수 A를 반드시 가져온다.")
@Test
void constant(){
 //when
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();
    //then
    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
}
  • 또는 문자를 더하는 "concat(xxx)"을 사용하여 문자를 이어 붙힐 수가 있는데,
    만일 같은 타입의 문자열을 붙히기 원한다면, "stringValue()"를 사용하여 문자형으로 변환할 수 있다!
    • 이 메서드는 ENUM 타입을 처리할 때도 쓰이지 잘 알아두면 좋을 것 같다!
@DisplayName("문자열을 더한다.")
@Test
void concat(){
 //when
    List<String> result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("member1"))
            .fetch();
    //then
    for (String o : result) {
        System.out.println("o = " + o);
    }
}

 


결론

지금까지 다양한 기본 문법들을 알아보며 실습을 진행해보았다.

다음 포스팅에는 좀 더 깊이 들어가서 중급 문법을 학습해보는 시간을 가져보도록 하겠다.

:)