본문 바로가기

QueryDSL

[QueryDSL] 프로젝션과 결과 반환

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

프로젝션: select 대상 지정

 

1. 프로젝션 대상이 하나

 List<String> result = queryFactory
     .select(member.username)
     .from(member)
     .fetch();
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음.
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회

 2. 튜플 조회 - 프로젝션 대상이 둘 이상일 때 사용

@Test
public void tupleProjection() {
    // `com.querydsl.core.Tuple`
    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);
    }
}

 


2️⃣ 프로젝션과 결과 반환 - DTO 조회 🌟

1. 순수 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;
    }
}
@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이름을 다 적어줘야해서 지저분함
  • 생성자 방식만 지원

2. QueryDSL 빈 생성(Bean population) - 결과를 DTO 반환할 때 사용

3가지 방법 지원

  1. 프로퍼티 접근
  2. 필드 직접 접근
  3. 생성자 사용

1. 프로퍼티 접근 - Setter

@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);
    }
}
  • 이 경우 DTO에 기본 생성자가 필요 - 기본 생성자로 DTO를 만들고 Setter 메서드로 집어넣기 때문

2. 필드 직접 접근

@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);
    }
}
  • 이 경우에도 DTO에 기본 생성자가 필요 - 기본 생성자로 DTO를 만들어야 하기 때문
  • 하지만 필드에 바로 값을 꽂아버리기 때문에 DTO에 @Setter가 없어도 동작함.

3. 생성자 사용

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

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}
  • 파리미터로 보내는 타입과 순서가 DTO의 생성자 타입과 순서에 일치해야 한다는 주의점이 있음.
  • 예시로, 위에 주석과 같이 값이 잘못 들어가도 컴파일 시점에서 오류를 잡아내지 못하기 때문에 주의해야 함.

🚀 별칭이 다를 때

@Data
@NoArgsConstructor
public class UserDto {

    private String name;
    private int age;

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
@Test
public void findUserDtoByField() {
    List<UserDto> result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

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

 

결과

userDto = UserDto(name=null, age=10)
userDto = UserDto(name=null, age=20)
userDto = UserDto(name=null, age=30)
userDto = UserDto(name=null, age=40)

 

🤔 그렇다면 어떻게 해야할까?

@Test
public void findUserDtoByField() {

    QMember memberSub = new QMember("memberSub");
    List<UserDto> result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username.as("name"),
                    ExpressionUtils.as(
                            JPAExpressions
                                    .select(memberSub.age.max())
                                    .from(memberSub), "age")
            ))
            .from(member)
            .fetch();

    for (UserDto userDto : result) {
        System.out.println("userDto = " + userDto);
    }
}
  • 프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안
  • ExpressionUtils.as(source,alias): 필드나, 서브 쿼리에 별칭 적용
  • username.as("memberName") : 필드에 별칭 적용
  • ✅ 참고로 생성자 방식은 이름이 아니라 타입을 보고 값이 들어가기 때문에 별칭을 줄 필요가 없음!

2️⃣ 프로젝션과 결과 반환 - @QueryProjection

생성자 + @QueryProjection

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • DTO 생성자에 @QueryProjection ./gradlew compileJava QMemberDto 생성 확인

✏️ @QueryProjection 활용

@Test
public void findDtoByQueryProjection() {
    List<MemberDto> result = queryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();
    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

 

  • 이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다.
  • 다만 DTO에 QueryDSL 어노테이션을 유지해야 하는 점과 DTO까지 Q 파일을 생성해야 하는 단점이 있다.
  • 즉, MemberDto가 QueryDSL의 의존성을 가지게 된다는 단점이 있다.

 

'QueryDSL' 카테고리의 다른 글