본문 바로가기
JPA/QueryDSL

[QueryDSL] 3. 중급 문법

by yoon_seon 2023. 5. 29.

중급 문법

1. 프로젝션과 결과 반환 - 기본

2. 프로젝션과 결과 반환 - DTO 조회

3. 프로젝션과 결과 반환 - @QueryProjection

4. 동적 쿼리 - BooleanBuilder 사용

5. 동적 쿼리 - Where 다중 파라미터 사용

6. 수정, 삭제 벌크 연산

7. SQL function 호출하기


📌 프로젝션과 결과 반환 - 기본

프로젝션이란 select절에 조회할 대상을 지정하는 것이다.

 

프로젝션 대상이 하나

List<String> result = queryFactory
       .select(member.username)
       .from(member)
       .fetch();
  • 프로젝션 대상이 하나인 경우 타입을 명확하게 지정할 수 있다. → List<String>
  • 프로젝션 둘 이상이면 튜플*이나 DTO로 조회한다.
    튜플* : querydsl이 여러 개의 프로젝션을 조회할 때를 대비해서 만들어 둔 타입이라고 생각하면 된다.

 

튜플 조회

프로젝션 대상이 둘 이상일 때

com.querydsl.core.Tuple

@Test
public void tupleProjection() {
    List<Tuple> result = queryFactory
            .select(member.username, member.age)
            .from(member)
            .fetch();

    for (Tuple tuple : result) {
        String username = tuple.get(member.username);
        Integer age = tuple.get(member.age);
        System.out.println("username : "+username);
        System.out.println("age : "+age);
    }
}
  • Tuple은 querydls이 제공하는 자료구조이다.
참고 : Tuple은 리포지토리 계층에서만 사용해야 한다. querydsl에 종속적인 자료구조 이기 때문에 그밖의 계층에서 사용할 경우 다른 데이터베이스 연결 방법을 변경되면 영향이 있을 수 있다. 그밖의 계층에서는 DTO로 변환해서 사용하자.

 


📌 프로젝션과 결과 반환 - DTO 조회

순수 JPA에서 DTO 조회

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 

순수 JPA에서 DTO 조회 코드

@Test
public void findDtoByJPQL() {
    List<MemberDto> result = em.createQuery(
                    "select new study.querydsl.dto.MemberDto(m.username, m.age) " +
                    "from Member m", MemberDto.class)
            .getResultList();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = "+memberDto);
    }
}
  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야한다.
  • DTO의 package 이름을 모두 적어야 하는 번거로움이 있다.
  • 생성자 방식만 지원한다.

querydsl은 이 모든 문제점을 극복한 깔끔한 방법을 제공한다.

 

Querydsl 빈 생성(Bean puplation)

결과를 DTO로 반환할 때 사용하며 3가지 방법이 존재한다.

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용

프로퍼티 접근

@Test
public void findDtoBySetter() {
    List<MemberDto> result = queryFactory
            .select(Projections.bean(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto : "+memberDto);
    }
}
  • Projections.bean(주입대상 클래스, 프로퍼티 1, 프로퍼티 2...)
  • dto의 setter 메서드를 통해 값이 주입된다.

 

필드 직접 접근

@Test
public void findDtoByField() {
    List<MemberDto> result = queryFactory
            .select(Projections.fields(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto : "+memberDto);
    }
}
  • Projections.bean(주입대상 클래스, 필드 1, 필드 2...) 
  • Getter, Setter를 무시하고 DTO 필드에 값이 직접 주입된다.

 

필드 직접 접근 - 별칭이 다를 때

@Test
public void findDtoByUserDto() {
    QMember memberSub = new QMember("memberSub");
    List<UserDto> result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username.as("name"),
                    member.age,
                    ExpressionUtils.as(JPAExpressions.
                            select(memberSub.age.max())
                            .from(memberSub), "maxAge") // 서브쿼리 별칭
            ))
            .from(member)
            .fetch();

    for (UserDto userDto : result) {
        System.out.println("userDto : "+userDto);
    }
}

@Data
public class UserDto {
    private String name;
    private int age;
    private int maxAge;
}
  • 프로퍼티, 필드 접근 생성 방식에서 이름이 다를 때 as("name")을 이용하여 해결한다.
  • ExpressionUtils.as(source, alias) : 필드나, 서브쿼리에 별칭 적용
  • username.as("memberName") : 필드에 별칭 적용

 

생성자 사용

@Test
public void findDtoByConstructor() {
    List<MemberDto> result = queryFactory
            .select(Projections.constructor(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto : "+memberDto);
    }
}
  • Projections.constructor(주입대상 클래스, 생성자 파라미터 1, 생성자 파라미터 2...) 
  • 생성자 파라미터 타입 및 순서와 일치하도록 조회 대상의 타입 및 순서를 맞춰줘야 한다.

 


📌 프로젝션과 결과 반환 - @QueryProjection

프로젝션 결과를 반환할 DTO의 생성자에 @QueryProjection을 붙여준 뒤

gradle → tasks → other → compileQuerydsl을 통해 DTO도 Q타입으로 생성해 준다.

 

DTO 생성자에 @QueryProjection 적용

package study.querydsl.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • 생성자에 @QueryProjection 적용
  • ./gradlew compileQuerydsl 실행
  • QMemberDto 생성 확인

 

@QueryProjection 활용

@Test
public void findDtoByQueryProjection() {
    List<MemberDto> fetch = queryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : fetch) {
        System.out.println("memberDto : "+memberDto);
    }
}
  • 이 방법은 기존 프로젝션 결과 반환 방법에서 하지 못했던 타입 체크를 컴파일 시점에 알 수 있어 안전하게 개발할 수 있다는 큰 장점이 있다. 하지만 DTO에 QueryDSL 어노테이션을 적용하기 때문에 DTO가 QueryDSL에 의존해야 한다는 점과 DTO까지 Q파일을 생성해야 하는 단점이 있다.

 

distinct 사용

@Test
public void distinct_test() throws Exception{
    List<String> result = queryFactory
            .select(member.username).distinct()
            .from(member)
            .fetch();
}
  • distinct는 JPQL의 distinct와 같다.

 


📌 동적 쿼리 - BooleanBuilder 사용

querydsl에서의 동적 쿼리는 두 가지 방식으로 해결할 수 있다.

  • BooleanBuilder
  • Where 다중 파라미터 사용

 

BooleanBuilder

@Test
public void dynamicQuery_BooleanBuilder() throws Exception {
    String usernameParam = "member1";
    Integer ageParam = null;

    List<Member> result = searchMember1(usernameParam, ageParam);
    assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember1(String usernameCond, Integer ageCond) {
    BooleanBuilder builder = new BooleanBuilder();
    // 파라미터중 null인 데이터는 검색조건에 포함하지 않는다.
    if (usernameCond != null) {
        builder.and(member.username.eq(usernameCond));
    }
    if (ageCond != null) {
        builder.and(member.age.eq(ageCond));
    }

    return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
}
  • if문 조건을 통해 파라미터가 null이라면 builder에 추가하지 않는다.

  • 다음과 같이 필수 파라미터의 경우 BooleanBuilder를 초기화하면서 추가해 줄 수 있다.

 


📌 동적 쿼리 - Where 다중 파라미터 사용

이 방법은 BooleanBuilder 방법보다 코드를 더 깔끔하게 작성할 수 있다.

@Test
public void dynamicQuery_WhereParam() throws Exception {
    String usernameParam = "member1";
    Integer ageParam = 10;

    List<Member> result = searchMember2(usernameParam, ageParam);
    assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            .where(usernameEq(usernameCond), ageEq(ageCond))
            .fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
    return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null ? member.age.eq(ageCond) : null;
}
  • where 조건에 null값은 무시된다는 것을 이용하여 동적쿼리를 처리한다.
  • where에 메서드를 인자로 넣어 메서드의 조건이 null이면 해당 파라미터는 조건에 포함되지 않고 null이 아니면 조건에 포함된다.
  • 메서드를 생성해야 한다는 번거로움 있지만 메서드를 다른 쿼리에서도 재사용할 수 있다는 매우 큰 장점이 있다.
  • 쿼리 자체의 가독성이 높아져 편리하다.

 

메서드를 조합하여 사용 가능

private BooleanExpression usernameEq(String usernameCond) {
    return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null ? member.age.eq(ageCond) : null;
}

private BooleanExpression allEq(String usernameCond, Integer ageCond) {
    return usernameEq(usernameCond).and(ageEq(ageCond));
}
  • usernameEq(), ageEq() 메서드를 조합한 allEq()를 사용가능
  • null 체크는 주의해서 처리해야 한다.

 


📌 수정, 삭제 벌크 연산

쿼리 한 번에 대량 데이터를 수정

@Test
public void bulkUpdate() throws Exception {
    // member1 = 10 -> 비회원
    // member2 = 20 -> 비회원
    // member3 = 30 -> member3
    // member4 = 40 -> member4

    long count = queryFactory
            .update(member)
            .set(member.username, "비회원")
            .where(member.age.lt(28))
            .execute();
    em.flush();
    em.clear();

    List<Member> result = queryFactory
            .selectFrom(member)
            .fetch();
    for (Member member1 : result) {
        System.out.println("member1 = " + member1);
    }
}

 

기존 숫자에 1 더하기

@Test
public void bulkAdd() throws Exception {
    long count = queryFactory
            .update(member)
            .set(member.age, member.age.add(1))
            .execute();
}

 

쿼리 한번에 대량 데이터 삭제

@Test
public void bulkDelete() throws Exception {
    long execute = queryFactory
            .delete(member)
            .where(member.age.gt(18))
            .execute();
}
참고 : JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화하는 것이 안전하다.

 


📌 SQL function 호출하기

SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.

 

Replace function 사용

@Test
public void sqlFunction() {
    List<String> result = queryFactory
            .select(
                    Expressions.stringTemplate("function('replace', {0}, {1}, {2})",
                            member.username, "member", "M"))
            .from(member)
            .fetch();

    for (String s : result) {
        System.out.println("s : "+s);
    }
    
    /*
    [결과]
    s : M1
    s : M2
    s : M3
    s : M4
    */
}
  • replace 된 결과를 확인할 수 있다.

 

lower function 사용

@Test
public void sqlFunction2() throws Exception {
    List<String> result = queryFactory
            .select(member.username)
            .from(member)
//          .where(member.username.eq(
//                    Expressions.stringTemplate("function('lower', {0})", member.username)))
            .where(member.username.eq(member.username.lower()))
            .fetch();

    for (String s : result) {
        System.out.println("s : "+s);
    }
}
  • lower 같은 ansi 표준 함수들은 querydsl이 상당 부분 내장하고 있다. 따라서 주석처리 된 where와 처리되지 않은 where 모두 동일하게 처리된다.

 


해당 글은 인프런의 [실전! Querydsl] 강의를 정리한 내용입니다.

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런

www.inflearn.com

댓글