QueryDSL 기본 문법 총정리

기본 문법들에 대해 설명하기 앞서 QueryDSL을 사용하기 위한 세팅 방법이나 사용방법들에 대해 알고 싶다면 아래의 포스팅들을 참고하면 된다.

2023.07.10 - [오픈소스] - QueryDSL 사용법, DTO 반환 방법 정리

2023.06.27 - [오픈소스] - [QueryDSL] 스프링부트 3.x 버전에서 QueryDSL 설정하기


QueryDSL 문법에 대해 설명하기 앞서

QueryDSL은 JPAQueryFactory를 사용하여 JPQL을 생성하고 실행한다.

작성하는 쿼리문을 보면 JPQL 문법이랑 동일하고, 뒤에 . 을 붙여 체인으로 연결하여 java 코드로 작성한다.

    // static import 방법으로 작성
    import static study.querydsl.entity.QMember.member;

    @Autowired
    private JPAQueryFactory queryFactory;

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

 

JPQL에 대한 이해가 필요하기 때문에 JPQL을 모른다면 아래 포스팅을 참고하는 것을 권장한다.

 

[JPA] JPQL이란? 사용방법, 기본 문법 총 정리

JPQL (Java Persistence Query Language)이란 ? JPQL은 엔티티 객체를 대상으로 하는 쿼리 언어이다. JPQL의 문법을 보면 다음과 같다. (나이가 18살 이상인 유저를 조회) select u from User as u where u.age > 18 JPQL은 SQL

hstory0208.tistory.com

 

우선, QueryDSL을 사용하기 위해서는 JPAQueryFactory를 Bean 등록하고 의존성 주입받아야 한다.

이 포스팅에서는 위 과정이 적용되어 있다고 가정하고 설명하겠다.

(이 부분에 대한 설명이 필요하다면 위의 QueryDSL 사용법 링크 참고)

 


fetch()

QueryDSL은 fetch를 이용해서 querydsl의 결과를 반환할 수 있다.

 

fetch()

리스트로 결과를 반환하는 방법이다.

만약에 데이터가 없으면 빈 리스트를 반환해준다.

    List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();

 

fetchOne()

단건을 조회할 때 사용한다.

결과가 없을때는 null 을 반환하고 결과가 둘 이상일 경우에는 NonUniqueResultException을 던진다.

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

검색 조건 쿼리

and, or

and, or 검색 조건.and(), .or()를 체인으로 연결하여 사용할 수 있다.

 Member findMember = queryFactory
    .selectFrom(member) // select, from을 합친 것
    .where(member.username.eq("홍길동")
    .and(member.age.eq(20)))
    .fetchOne();

 

and의 경우에는 쉼표( , )를 사용하여 and 조건을 추가할 수 있다.

 List<Member> result1 = queryFactory
    .selectFrom(member)
    .where(member.username.eq("홍길동"), member.age.eq(20))
    .fetch();

이 경우에는 조건에 해당하는 결과가 없어 중간에 null이 들어가는 경우에는 null을 무시한다.

원래 and 조건은 and 로 연결된 모든 조건이 만족하는 조건을 반환하지만,

이 코드에서 만약 "홍길동"이라는 유저가 없다면 이 조건이 무시되고, 나이가 20살인 유저들을 반환하게 되는 것이다.

 

이외의 검색 조건
    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%’ 검색

정렬

아래의 쿼리는 나이가 20살인 회원들을 조회한 결과 값을 나이를 기준으로 내림차순(desc), 이름을 기준으로 오름차순(asc) 정렬한 쿼리이다.

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

여기서 member.username.asc()뒤에 붙은 .nullsLast()은 member.username.asc()의 결과가 null이 있으면 null을 마지막에 출력하는 것이고,

반대로 .nullsFirst()는 null을 처음에 출력하는 것이다.


집합

QueryDSL은 JPQL이 제공하는 모든 집함 함수를 제공한다.

    List<Tuple> result = queryFactory
        .select(member.count(), // member 수
                member.age.sum(), // member 나이 합
                member.age.avg(), // member 나이 평균
                member.age.max(), // member 최대 나이
                member.age.min()) // member 최소 나이
        .from(member)
        .fetch();

 

GroupBy

다음은 팀별로 구성원의 평균 연령과 팀 이름을 조회하는 쿼리이다.

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

 

having

그룹화된 결과에 조건을 달려면 having()을 추가할 수 있다.

다음은 각 팀의 평균 연령이 19살이 넘는 팀의 이름과, 평균 나이를 조회하는 쿼리이다.

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

join

QueryDSL에서 join은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q 타입을 지정한다.

 join(조인 대상, 별칭으로 사용할 Q타입) 

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

여기서 join은 기본값으로 innerJoin()이며, leftJoin(), rightJoin()도 제공한다.

세타 조인

서로 연관관계가 없는 엔티티도 조인할 수 있는 방법으로 .join을 사용하는 게 아니라, .from 절에 여러 엔티티를 선택해서 세타 조인한다.
단점으로는 외부 조인이 불가능하지만, 조인 on 절을 사용하면 외부 조인이 가능하다.


join - on 절

on 절은 조인 대상 필터링연관관계가 없는 엔티티 외부 조인에 사용된다.

 

조인 대상 필터링

다음은 멤버와 팀을 조인하면서, 팀 이름이 "teamA"인 팀만 조인하고 회원은 모두 조회하는 쿼리이다.

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

 

결과를 보면 left 조인으로 Member는 모두 조회하되, Team의 이름이 teamA인 Team만 조회하고, 아닐 시 null로 반환된것을 볼 수 있다.

 

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

위에서 세타 조인 경우에는 외부 조인을 사용할 수 없다고 했었다.

이 경우 다음과 같이 on 절을 사용해 외부 조인을 적용할 수 있다.

 

기존의 join과의 차이가 있는데, 

아래 쿼리를 보면 leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어갔다.

이렇게 사용할 경우, 일반 join과 다르게 id로 매칭하는 것이아니라 on절 조건에 해당하는 것만 조인한다.

 

다음은 회원의 이름이 팀 이름과 같은 대상 외부 조인하는 쿼리 이다.

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

 

on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.

따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.

 


fetch join (페치 조인)

페치 조인이란 연관된 엔티티를 SQL 한번에 (한방 쿼리) 조회하는 기능이다.

페치 조인에 대해 알고 싶다면 아래 링크를 참고하는 것을 추천한다.

 

[JPA] JPQL의 fetch join(패치 조인)이란?

Fetch Join JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션들을 한번의 SQL 쿼리로 함께 조회할 수 있다. 연관된 엔티티에 대해 추가적인 쿼리를 실행할 필요 없이 효율적인

hstory0208.tistory.com

 

페치 조인을 적용하는 방법은 간단하다. join()문 뒤에 fetchJoin()을 추가하기만 하면 된다.

    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin() // 한방 쿼리
            .where(member.username.eq("member1"))
            .fetchOne();

서브 쿼리

JPQL은 from 절의 서브쿼리는 지원하지 않는다.  당연히 Querydsl 도 지원하지 않는다.

select 절의 서브쿼리와 where절의 서브쿼리만 가능하다.

 

QueryDSL에서 서브쿼리를 사용하기 위해서는 JPAExpressions 클래스를 사용해야 한다.

where절에 서브쿼리

다음은 where절에 서브쿼리를 사용나이가 10살보다 많은 회원들을 조회하는 쿼리이다.

여기서 보면 QMember를 하나더 생성한 것을 볼 수 있는데, 서브쿼리가 static import한 member와 겹치지 않도록 memberSub 생성한 것이다.

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

 

select절 서브쿼리

다음은 select 절에 서브쿼리를 사용해 나이가 평균 나이인 회원들을 조회하는 쿼리이다.

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

 


case 문

QueryDSL의 case문의 문법은 기본 SQL 문법과 똑같다.

  • when() : 괄호안에 조건문 작성
  • then() : when절이 true일 경우 실행
  • otherwise() : when절이 false 일 경우 실행

 

단순한 조건 사용 시
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("열살")
                    .when(20).then("스무살")
                    .otherwise("기타"))
            .from(member)
            .fetch();

 

복잡한 조건 사용 시

복잡한 조건 사용시 다음과 같이 CaseBuilder()를 사용하여 복잡한 조건의 case문을 작성할 수 있다.

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

상수 사용

QueryDSL에서는 Expressions.constant()를 사용해 상수를 활용할 수 있다.

상수를 쿼리 조회 결과에 담아 출력
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("VIP 회원"))
            .from(member)
            .fetch();

위 쿼리의 실행결과를 보면 select절에서 선택한 대상과, 작성한 상수가 같이 조회되는 것을 볼 수 있다.


문자 더하기

기본 SQL 문법과 동일하게 문자를 concat을 사용해 합칠 수 있다.

 

다음은 회원 이름과 나이를 "_"로 합치는 쿼리이다.

여기서 주의할점으로는 concat은 문자만 합칠 수 있는데 age는 Integer 타입이다.

그렇기 때문에 age를 stringValue() 를 사용해 문자로 변환하였다.

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


동적 쿼리 작성 방법
 

QueryDSL 동적 쿼리를 작성하는 방법에 대해 알아보자.

QueryDSL을 사용하는 이유는 아마 동적 쿼리를 아주 쉽게 해결할 수 있어서가 아닐까 싶다. 이번 포스팅에서는 QueryDSL을 사용해서 동적 쿼리를 작성하는 방법에 대해 포스팅해 보려한다. QueryDSL 설

hstory0208.tistory.com

 

QueryDSL 페이징 
 

QueryDSL 페이징 처리

 

hstory0208.tistory.com