본문 바로가기

JPA

[Querydsl] Querydsl 적용하기

Querydsl 사용이유

SpringBootSpring Data JPA의 조합으로 개발자가 직접 SQL 문을 작성할 필요없이 메소드로 동일한 기능을 제공해주기 때문에 매우 편리해졌지만 아직도 복잡한 쿼리와 동적 쿼리를 작성하는데는 한계가 있다.

 

Querydsl은 위와 같은 문제점을 해결해주고 여러가지 편리한 기능들을 제공해준다.

 

아래 코드는 member1이라는 username을 갖는 Member를 조회하는 기능을 JPQL과 Querydsl을 이용하여 테스트한 코드이다. 먼저 JPQL의 경우 sql문을 String 타입으로 작성했지만 Querydsl은 자바코드로 작성됐다. 그렇기 때문에 JPQL은 해당 기능을 사용하기 전에는 무엇이 잘못되었는지 확인할 수 없지만(런타임 에러) Querydsl은 컴파일 시점에 문법을 체크할 수 있고 자동완성 기능까지 제공해주기 때문에 JPQL을 이용한 방법보다 편리하다. 

@Test
public void startJPQL() {
    // member1을 찾아라
    String qlString = "select m from Member m " +
            "where m.username = :username";

    Member findMember = em.createQuery(qlString, Member.class)
            .setParameter("username", "member1")
            .getSingleResult();

    assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
    QMember m = new QMember("m");

    Member findMember = queryFactory
            .select(m)
            .from(m)
            .where(m.username.eq("member1"))
            .fetchOne();

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

뿐만 아니라 JPQL을 사용하면 DTO 매핑시 패키지 명까지 작성해주어야 하지만 Querydsl을 사용하면 프로젝션을 이용해 쉽게 DTO 매핑을 수행할 수 있고 동적쿼리작성도 보다 쉽게 작성할 수 있다.

Querydsl 기본 문법

기본 Q-Type 활용

  1. QMember member = new QMember("m")
  2. QMember member = QMember.member
  3. Static import 사용 
import static study.querydsl.entity.QMember.member;

...

Member findMember = queryFactory
        .select(member)
        .from(member)
        .where(member.username.eq("member1"))
        .fetchOne();

같은 테이블을 조인해서 조회해야 하는 경우 직접 Q타입 객체를 생성해서 사용한다.

검색 조건 쿼리

  • member.username.eq("member1") : username이 member1인 것만 조회
  • member.username.ne("member1") : username이 member1이 아닌것만 조회
  • member.username.eq("member1").not() : username이 member1이 아닌것만 조회
  • member.username.isNotNull() : username이 null이 아닌것만 조회
  • member.age.in(10, 20) : 나이가 10이거나 20인 것만 조회
  • member.age.notIn(10, 20) : 나이가 10이거나 20이 아닌것만 조회
  • member.age.between(10,30) : 나이가 10에서 30 사이인 것만 조회
  • member.age.goe(30) : 나이가 30보다 크거나 같은 것만 조회
  • member.age.gt(30) : 나이가 30보다 큰것만 조회
  • member.age.loe(30) : 나이가 30보다 작거나 같은 것만 조회
  • member.age.lt(30) : 나이가 30보다 작은것만 조회
  • member.username.like("member%") : username이 member로 시작하는 것만 조회
  • member.username.contains("member") : username이 member를 포함하고 있는 것만 조회
  • member.username.startsWith("member") : username이 member로 시작하는 것만 조회

and(), or() 메서드 체인으로 연결하여 여러 검색조건을 추가할 수 있으며 and()만 있는 경우 콤마(,)를 사용해 연결할 수 있다.

Member findMember = queryFactory
        .select(member)
        .from(member)
        .where(
                member.username.eq("member1"),
                member.age.eq(10)
        )
        .fetchOne();

결과 조회

  • fetch() : 리스트 조회, 데이터가 없으면 빈 리스트를 리턴한다.
  • fetchOne() : 단건 조회, 데이터가 없으면 null을 리턴하고 데이터가 두 개 이상이면 NonUniqueResultException 에러가 발생한다.
  • fetchFirst() : 조회된 데이터의 첫 한건만 리턴해준다. limit(1).fetchOne()
//List
List<Member> memberList = queryFactory
        .selectFrom(member)
        .fetch();

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

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

fetchResults()와 fetchCount()는 deprecated 됨

정렬

  • orderBy() 메소드를 이용해 데이터를 정렬한다.
  • 콤마(,)를 이용해 정렬기준 추가 가능
  • asc() : 오름차순
  • desc() : 내림차순
  • nullsFirst() : null일 경우 처음에 출력
  • nullsLast() : null일 경우 마지막에 출력
List<Member> result = queryFactory
        .selectFrom(member)
        .where(member.age.eq(100))
        .orderBy(member.age.desc(), member.username.asc().nullsLast())
        .fetch();

페이징

  • offset(1) : 1번 인덱스부터 조회, 0번부터 시작
  • limit(2) : 최대 2건 조회
List<Member> result = queryFactory
        .selectFrom(member)
        .orderBy(member.username.desc())
        .offset(1)
        .limit(2)
        .fetch();

집합

  • select로 여러가지 데이터를 조회올 때 Querydsl이 제공하는 튜플로 조회를 해온다.
  • groupBy() : 데이터를 그룹화하여 조회해온다.
  • having() : groupBy()로 그룹화된 데이터를 제한할 떄 사용한다.
    • ex) having(item.price.gt(1000)) => 가격이 1000원 보다 비싼 아이템만 그룹화한다.
List<Tuple> result = queryFactory
        .select(team.name, member.age.avg())
        .from(member)
        .join(member.team, team)
        .groupBy(team.name)
        .fetch();
        
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);

조인

기본조인

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

// member.team = 조인 대상
// team = 별칭으로 사용할 Q타입 객체
List<Member> result = queryFactory
        .selectFrom(member)
        .join(member.team, team)
        .where(team.name.eq("teamA"))
        .fetch();

세타조인 == 막조인

  • 연관관계가 없는 필드로 조인하는 방법으로 from 절에 여러 엔티티를 나열하여 사용한다.
  • 원래는 외부조인이 불가능하지만 on 을 사용하면 외부조인도 가능하다.
List<Member> result = queryFactory
        .select(member)
        .from(member, team)
        .where(member.username.eq(team.name))
        .fetch();

on 절

  • 조인 대상을 필터링할때 사용한다.
  • 연관관계가 없는 엔티티를 외부조인할때 사용한다.
  • on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다. => 내부조인을 사용하면 on 절 보다는 익숙한 where 절을 사용하자
// 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
List<Tuple> result = queryFactory
        .select(member, team)
        .from(member)
        .leftJoin(member.team, team).on(team.name.eq("teamA"))
        .fetch();

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

일반조인과 다르게 on 절을 사용한 연관관계 없는 엔티티 외부 조인에서는 join 파라미터에 엔티티가 하나만 들어간다.

// 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
List<Tuple> result = queryFactory
        .select(member, team)
        .from(member)
        .leftJoin(team).on(member.username.eq(team.name))
        .fetch();

페치조인

  • 페치조인은 N + 1 문제를 해결하기 위해 연관된 엔티티를 SQL 한번에 조회하는 기능으로 주로 성능 최적화에 사용하는 방법이다.
  • join 기능 뒤에 fetchJoin()을 붙여서 사용한다.
Member findMember = queryFactory
        .selectFrom(member)
        .join(member.team, team).fetchJoin()
        .where(member.username.eq("member1"))
        .fetchOne();

서브쿼리

JPAExpressions를 사용하여 구현할 수 있다.

 

where 절에서의 서브쿼리

// alias가 바깥에 있는 member와 겹치면 안되기 떄문에 직접 QMember 객체를 생성해서 사용한다.
// 나이가 가장 많은 회원 조회
QMember memberSub = new QMember("memberSub");

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

 

select 절에서의 서브쿼리

// 나이가 평균 나이 이상인 회원
QMember memberSub = new QMember("memberSub");

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

from 절에서는 서브쿼리를 사용할 수 없다. from 절에서 서브쿼리를 사용해야하는 경우 서브쿼리를 join으로 변경하거나 애플리케이션에서 쿼리를 2번 분리해서 사용할 수 있다. 두 가지 방법 모두 적용하기 힘든 경우에는 nativeSQL을 사용해야 한다.

CASE 문

단순한 조건

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

복잡한 조건

복잡한 조건의 경우 CaseBuilder()를 사용하여 구현할 수 있다.

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();

상수

상수를 함께 조회해야 하는 경우 Expressions.constant()를 사용하면 쉽게 조회할 수 있다.

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

문자 더하기

여러 필드의 값을 문자열로 더해서 조회해야하는 경우 stringValue()를 사용하여 문자열로 변환한 후 더한다.

List<String> result = queryFactory
        .select(member.username.concat("_").concat(member.age.stringValue()))
        .from(member)
        .fetch();

'JPA' 카테고리의 다른 글

[Querydsl] 동적쿼리 작성하기  (0) 2023.01.18
[Querydsl] 프로젝션  (0) 2023.01.17
[JPA] N + 1 문제와 해결방법  (0) 2022.10.19
[JPA] 객체지향 쿼리  (0) 2022.10.10
[JPA] 영속성 전이 (CASCADE), 고아객체 제거 (ORPHAN)  (0) 2022.10.05