본문 바로가기
JPA

Querydsl 기본 문법

by Heesu.lee 2021. 4. 26.

기본 Q-Type 활용

Q클래스 인스턴스를 사용하는 2가지 방법

QMember member = new QMember("m");// 직접 별칭 주어 새롭게 생성하여 사용
QMember member = QMember.member; // 이미 생성된 인스턴스 사용

 

새롭게 생성하여 사용하는 경우엔 생성자 안에 별칭을 넣어 사용하면 된다.

그러나, 기본적으로 생성된 인스턴스를 사용하는 것을 권장한다.

 

실제 사용 시 관련 Q Class 를 static import 하여 사용하는 것을 추천

import static study.querydsl.entity.QMember.*;

@DisplayName("QMember static import 하여 사용")
@Test
void querydsl() {
    // given: QMember
    String username = "member1";

    // when
    Member saved = queryFactory
            .select(member)
            .from(member)
            .where(member.username.eq(username))
            .fetchOne();

    // then
    assertThat(saved).isNotNull();
    assertThat(saved.getUsername()).isEqualTo(username);
}

※ 참고사항

위에서 살펴본 두가지 방식 중 기본적으로 생성된 인스턴스를 사용하되,

만약 같은 테이블을 조인하여 사용해야하는 경우 서로 다른 별칭을 주어 인스턴스 생성하여 사용 해야한다.

 

검색 조건 쿼리

SQL 에서 제공되는 대부분의 조건문들은 Querydsl 을 통해서 사용할 수 있다.

특히, 기존 JPQL 에서 제공되는 것들은 모두 사용할 수 있다고 보면 된다.

(Querydsl 로 작성된 쿼리는 결과적으로 JPQL 로 변환되어 요청되기 때문에)

 

@DisplayName("검색 조건 쿼리")
@Test
void search() {
    // given: QMember
    int age = 10;
    String username = "member1";

    // when
    Member saved = queryFactory
            .selectFrom(member)
            .where(member.username.eq(username)
                    .and(member.age.eq(age)))
            .fetchOne();

    // then
    assertThat(saved).isNotNull();
    assertThat(saved.getUsername()).isEqualTo(username);
    assertThat(saved.getAge()).isEqualTo(age);
}

 

참고로, and 조건의 경우 파라미터 단위로 끊어서 사용 가능하다

// when
Member saved = queryFactory
        .selectFrom(member)
        .where(member.username.eq(username), member.age.eq(age))
        .fetchOne();

※ 참고 - and 조건을 파라미터로 활용 시 null 인 경우 해당 조건 무시함 (동적 쿼리 작성 시 유용함)

 

Querydsl 제공되는 검색 조건 예시

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.username.isNotNull() //이름이 is not null

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30

member.username.like("member%") //like 검색 
member.username.contains("member") // like ‘%member%’ 검색 
member.username.startsWith("member") //like ‘member%’ 검색

 

결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() : count 쿼리로 변경해서 count 수 조회
@DisplayName("결과 조회")
@Test
void resultFetch() {
    // 리스트
    List<Member> members = queryFactory.selectFrom(member)
            .fetch();

    // 단건
    Member fetchOne = queryFactory.selectFrom(QMember.member)
            .fetchOne();

    // 처음 한건만 조회
    Member fetchFirst = queryFactory.selectFrom(QMember.member)
            .fetchFirst();

    // 페이징에서 사용
    QueryResults<Member> results = queryFactory.selectFrom(QMember.member)
            .fetchResults();

    long total = results.getTotal(); // total count 용 쿼리 추가 발생 (복잡한 경우 따로 count 쿼리 사용 추천)
    List<Member> content = results.getResults();

    // count 쿼리로 변경
    long count = queryFactory.selectFrom(QMember.member)
            .fetchCount();
}

 

정렬

@DisplayName("정렬 예시")
@Test
void sort() {
    // given
    em.persist(new Member(null, 100));
    em.persist(new Member("member5", 100));
    em.persist(new Member("member6", 100));

    List<Member> results = queryFactory.selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.desc(), member.username.asc().nullsLast())
            .fetch();

    assertThat(results).isNotNull();

    Member member5 = results.get(0);
    Member member6 = results.get(1);
    Member memberNull = results.get(2);

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

예시 코드 정렬 조건으로는 다음과 같다.

  1. 회원 나이 내림차순
  2. 회원 이름 오름차순
  3. 만약 이름이 없는 경우 맨 마지막에 출력(nulls last)

※ 참고 - nullsFirst 조건도 존재한다.

 

페이징

@DisplayName("기본 페이징")
@Test
void paging1() {
    // given
    int offset = 1;
    int limit = 2;

    // when
    List<Member> result = queryFactory.selectFrom(member)
            .orderBy(member.username.desc())
            .offset(offset) // 0부터 시작
            .limit(limit) // 요청 개수
            .fetch();

    // then
    assertThat(result).isNotNull();
    assertThat(result.size()).isEqualTo(limit);
}

@DisplayName("페이징 - 전체 개수가 필요한 경우")
@Test
void paging2() {
    // given
    int offset = 1;
    int limit = 2;

    // when
    QueryResults<Member> result = queryFactory.selectFrom(member)
            .orderBy(member.username.desc())
            .offset(offset) // 0부터 시작
            .limit(limit) // 요청 개수
            .fetchResults();

    // then
    assertThat(result).isNotNull();
    assertThat(result.getTotal()).isEqualTo(4);
    assertThat(result.getLimit()).isEqualTo(limit);
    assertThat(result.getOffset()).isEqualTo(offset);
    assertThat(result.getResults().size()).isEqualTo(2);
}

페이징 쿼리 사용 시 유의 사항

fetchResult 의 경우 count 쿼리가 추가로 요청된다.

그렇지만, 복잡한 페이징인 경우 (where, join 등) 따로 더 간단하게 count 쿼리 작성할 수 있는 경우,

따로 count 쿼리 분리하여 사용하는게 성능 개선에 효과를 줄 수 있다.

 

 

집합

@DisplayName("집합")
@Test
void aggregation() {
    // given: QMember

    // when
    Tuple result = queryFactory
            .select(
                member.count(),
                member.age.sum(),
                member.age.avg(),
                member.age.max(),
                member.age.min()
            )
            .from(member)
            .fetchOne();

    // then
    assertThat(result).isNotNull();
    assertThat(result.get(member.count())).isEqualTo(4);
    assertThat(result.get(member.age.sum())).isEqualTo(100);
    assertThat(result.get(member.age.avg())).isEqualTo(25);
    assertThat(result.get(member.age.max())).isEqualTo(40);
    assertThat(result.get(member.age.min())).isEqualTo(10);
}

@DisplayName("GroupBy 사용 - 팀 이름과 각 팀의 평균 연령")
@Test
void groupBy() {
    // given

    // when
    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();

    // then
    Tuple teamA = result.get(0);
    Tuple teamB = result.get(1);

    assertThat(teamA.get(team.name)).isEqualTo("teamA");
    assertThat(teamA.get(member.age.avg())).isEqualTo(15);

    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}

 

특이사항으로는 기본적으로 Tuple 객체를 반환한다

원하는 결과값은 해당 expression 을 get() 메서드에 넘겨주어 확인할 수 있다.

 

그렇지만 대게, Tuple 로 받아서 처리하기 보다 관련 DTO 로 받아 사용하는 것을 권장한다.(중급 사용에서 다룰 예정)

 

조인

기본 조인

@DisplayName("기본 조인 - Team A 에 소속된 모든 회원")
@Test
void join() {
    // given: QMember, QTeam

    // when
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

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

join 뿐만 아니라, leftJoin, rightJoin, innerJoin 등 모두 사용 가능하다.

 

참고 사항 - 연관관계가 없는 조인 가능 (세타 조인)

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

from 절에 여러 엔티티를 선택해서 연관관계가 없는 조인이 가능하다.

과거에는 외부 조인이 불가능하였으나(innerJoin 만 가능), 조인 on 을 사용하여 외부 조인 사용 가능

 

조인 - ON 절

ON 절을 활용한 조인은 참고로 JPA 2.1 부터 지원이 된다.

  • 조인 대상 필터링
  • 연관관계 없는 엔티티 외부 조인

조인 대상 필터링

@DisplayName("회원과 팀을 조인하면서, 팀 이름이 teamA 인 팀만 조인, 회원은 모두 조회")
@Test
void join_on_filtering() {
    // given
    String team = "teamA";

    // when
    List<Tuple> results = queryFactory
            .select(member, QTeam.team)
            .from(member)
            .leftJoin(member.team, QTeam.team).on(QTeam.team.name.eq(team))
            .fetch();

    // then
    assertThat(results).isNotNull();
    results.forEach(result -> System.out.println("result = " + result));
}

ON 을 활용한 필터링은 외부 조인 인 경우에만 의미가 있다.

내부 조인을 하는 경우 WHERE 조건을 통해 해당 필터링을 동일하게 적용할 수 있다.

외부 조인이 필요한 경우에만 ON 절을 활용하고, 내부 조인인 경우 WHERE 을 활용하는 방식을 권장

 

// 내부 조인 ON 절 사용
List<Tuple> results = queryFactory
        .select(member, team)
        .from(member)
        .join(member.team, team).on(team.name.eq(teamName))
        .fetch();
        
// 결과
result = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
result = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]

// 내부 조인 WHERE 절 사용
List<Tuple> results = queryFactory
        .select(member, team)
        .from(member)
        .join(member.team, team)
        .where(team.name.eq(teamName))
        .fetch();
        
// 결과
result = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
result = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]

 

연관관계 없는 엔티티 외부 조인

@DisplayName("연관관계가 없는 조인")
@Test
void join_on_no_relation() {
    // given
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

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

    // then
    assertThat(result).isNotNull();
    result.forEach(tuple -> System.out.println("tuple = " + tuple));
}

실행 결과

외부 조인뿐만 아니라 내부 조인 역시 가능하다. (Hibernate5.1 부터 지원)

다만, 내부 조인인 경우 Team 이 null 인 경우 결과로 포함되지 않는다.

 

연관관계 없는 조인인 경우 문법이 조금 다른데, 연관관계가 있는 경우엔 leftJoin(member.team, team) 과 같이 작성하지만,

위처럼 연관관계가 없는 경우엔 from(member).leftJoin(team).on(조건) 조인 부분에 team 만 작성한다.

 

조인 - Fetch Join

Fetch Join 은 SQL 에서 제공하는 기능은 아니다.

JPQL 의 성능 튜닝을 위해 제공되는 조인 기능이며, 주로 성능 최적화에 사용하는 방식이다.

이는 SQL 조인을 활용해서 연관된 엔티티 혹은 컬렉션들을 한번에 조회하는 기능을 제공한다.

@DisplayName("페치조인 미적용")
@Test
void noFetchJoin() {
    // given: QMember
    em.flush();
    em.clear();

    String memberName = "member1";

    // when
    Member saved = queryFactory
            .selectFrom(member)
            .where(member.username.eq(memberName))
            .fetchOne();

    // then
    assertThat(saved).isNotNull();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(saved.getTeam());

    assertThat(loaded).as("페치 조인 미적용").isFalse();
}

@DisplayName("페치조인 적용")
@Test
void useFetchJoin() {
    // given: QMember
    em.flush();
    em.clear();

    String memberName = "member1";

    // when
    Member saved = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()
            .where(member.username.eq(memberName))
            .fetchOne();

    // then
    assertThat(saved).isNotNull();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(saved.getTeam());

    assertThat(loaded).as("페치 조인 적용").isTrue();
}

Fetch JOIN 을 적용하지 않은 경우 현재 Member 와 Team 의 연관관계 로딩 전략은 Lazy를 취하고 있기 때문에,

Member 조회 시 해당 Team 엔티티를 같이 조회하지 않는다. 실제 Team 엔티티에 접근 시 쿼리 요청을 통해 가져오는 방식을 취하고 있다.

즉, Member, Team 조회에 대한 SQL 쿼리가 각각 실행되는 것이다. (N+1 문제 야기)

 

반면, Fetch JOIN 을 적용한 경우 Member 에 필요한 Team 객체를 SQL 조인 쿼리로 한번에 조회하기 때문에,

조회 시점에서 바로 연관된 Team 엔티티에 접근할 수 있다.

 

사용 방법으로는 join(), leftJoin() 등의 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.

 

서브 쿼리

@DisplayName("WHERE 절 - 나이가 가장 많은 회원 조원")
@Test
void subQuery() {
    // given
    QMember memberSub = new QMember("memberSub");

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

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

@DisplayName("WHERE 절 - 나이가 평균 이상인 회원")
@Test
void subQueryGoe() {
    // given
    QMember memberSub = new QMember("memberSub");

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

    // then
    assertThat(result).extracting("age")
            .containsExactly(30, 40);
}

@DisplayName("IN 절 서브쿼리 사용하기")
@Test
void subQueryIn() {
    // given
    QMember memberSub = new QMember("memberSub");

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

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

@DisplayName("SELECT 절에 서브 쿼리 사용하는 경우")
@Test
void selectSubQuery() {
    // given
    QMember memberSub = new QMember("memberSub");

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

    // then
    assertThat(result).isNotNull();
    result.forEach(tuple -> System.out.println("tuple = " + tuple));
}

위 예제 코드에선 static import 하여 보이진 않지만,

서브쿼리는 쓰고자 하는 곳에 com.querydsl.jpa.JPAExpressions 사용하여 작성한다. (SELECT, WHERE, IN)

 

JPAExpressions.select().where().from()

 

여기서 주의해야할 사항으로는 기존에는 생성된 QClass 사용하였지만,

다른 별칭을 준 새로운 인스턴스를 사용해야 한다. (각 예제 서브 쿼리 중 meberSub 참고)

이는 SQL 작성 시 다른 별칭을 사용하는거와 동일한 원리이다.

 

추가로, 현재 JPA 및 Querydsl 은 from 절의 서브 쿼리 작성을 지원하지 않는다.

JPA 는 SELECT 절 쿼리 지원하지 않지만, Querydsl 은 Hibernate 구현체를 사용하여 해당 기능을 제공한다.

 

FROM 절의 서브쿼리 해결방안으로는 다음 3가지가 존재한다.

  • 서브 쿼리를 join 으로 변경한다. (가능한 상황인 경우)
  • 애플리케이션에서 쿼리를 2번 분리해서 실행한다.(성능상 이슈가 없을 시)
  • 마지막으로 nativeSQL 을 사용한다.
대부분 from 절에 서브 쿼리 쓰는 이유 - 화면 단에 맞추기위한 데이터를 가져오기 위해
화면에 맞춘 쿼리는 올바른 요청이 아니다. SQL 오로지 데이터 가져오는데 집중하는게 좋다.
복잡한 쿼리를 줄이기 위해 데이터만 집중하는게 중요하다 (필요한 데이터만 출여서 가져오는 것)

 

CASE 문

@DisplayName("단순한 조건")
@Test
void basicCase() {
    // given: QMember

    // when
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("열살")
                    .when(20).then("스무살")
                    .otherwise("기타"))
            .from(member)
            .fetch();

    // then
    assertThat(result).isNotNull();

    result.forEach(age -> System.out.println("age = " + age));
}

@DisplayName("복잡한 조건")
@Test
void complexCase() {
    // given: QMember

    // 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
    assertThat(result).isNotNull();

    result.forEach(age -> System.out.println("age = " + age));
}

JPQL 과 마찬가지로 CASE 문에 대해서 지원한다.

살펴볼 내용으로는 복잡한 CASE 문에 대해선 CaseBuilder 를 통해 정의하여 사용한다.

 

상수, 문자 더하기

@DisplayName("상수 집어넣기")
@Test
void constant() {
    // given

    // when
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();

    // then
    assertThat(result).isNotNull();

    result.forEach(tuple -> System.out.println("tuple = " + tuple));
}

@DisplayName("문자 더하기")
@Test
void concat() {
    // given: QMember

    // when 원하는 문자열 = {username}_{age}
    List<String> result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("member1"))
            .fetch();

    // then
    assertThat(result).isNotNull();

    result.forEach(member -> System.out.println("member = " + member));
}

 

상수 집어넣기에 경우 실제 요청되는 JPQL 엔 해당 부분이 포함되지 않는다.

문자 더하기에선 .stringValue() 부분을 많이 쓰게 되는데, 문자가 아닌 다른 타입들을 문자열 변환할 때 사용한다.

특히 ENUM 처리 시에 자주 사용된다.

 

참고

  • 해당 게시글은 김영한님의 실전! Querydsl 을 참고하여 작성되었습니다.
  • 보다 자세한 내용은 해당 강의를 보시고 확인하시길 적극 권장 드립니다.

'JPA' 카테고리의 다른 글

Querydsl 실무 활용편 (1)  (0) 2021.05.02
Querydsl 중급 문법  (0) 2021.04.28
Querydsl 사용 개요  (0) 2021.04.25
쿼리 메소드 기능  (0) 2021.01.26
객체지향 쿼리 언어  (0) 2021.01.25

댓글