[Querydsl:중급] 프로젝션

Welcome to Seoyun Dev Log·2023년 4월 28일
0

JPA

목록 보기
6/15

1. 프로젝션과 결과 반환

1) 기본 결과 반환

프로젝션: select 절에 어떤것을 조회할 것인지 대상을 지정하는 것

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

📌참고: tuple은 리포지토리 계층을 넘어서서 service나 controller 까지 넘어가는 것은 좋지 않다. resultSet, tuple 모두 동일하다.
리포지토리 계층 외부로 보낼 경우에는 DTO로 변환해서 보내야한다.


2) DTO 조회

  • MemberDto
package study.querydslpractice.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MemberDto {
    private String username;
    private int age;
}

1. 순수 JPA에서 DTO 조회

@DisplayName("findDtoByJPQL")
    @Test
    void findDtoByJPQL() {
        //new operation 활용 방법
        List<MemberDto> resultList = em.createQuery("select new study.querydslpractice.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
                .getResultList();
        for (MemberDto memberDto : resultList) {
            System.out.println(memberDto);
        }
    }
  • 문제점
    • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함
    • DTO의 package이름을 다 적어줘야해서 지저분함
    • 생성자 방식만 지원함

2. Querydsl Setter

  • Setter를 통해서 주입되는 것
  • 기본 생성자를 생성해 줘야한다.
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

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

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public int getAge() {
        return age;
    }
}

Projections.bean()

@DisplayName("findDtoBySetter")
    @Test
    void findDtoBySetter() {
        List<MemberDto> dtoList = jpaQueryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }

3. Querydsl Field

  • Getter, Setter 상관없이 바로 필드에 값을 주입하는 것

Projections.Fields()

@DisplayName("findDtoByField")
    @Test
    void findDtoByField() {
        List<MemberDto> dtoList = jpaQueryFactory
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }

4. Querydsl constructor

  • 생성자 타입이 순서에 맞게 맞아야 한다.
  • 📌만약 타입을 더 추가해서 넣는 경우에도 일단 실행은 된다.
    컴파일 에러로 잡아내지 못한다.따라서 런타임 에러가 발생하여 실제 유저가 해당 코드를 실행할 때 에러를 찾아낼 수 있다. 치명적인 에러
@DisplayName("findDtoByConstructor")
    @Test
    void findDtoByConstructor() {
        List<MemberDto> dtoList = jpaQueryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }

📌5. field 조회시 별칭이 맞지 않는 경우

생성자 주입의 경우 매개변수의 수와 타입순서만 맞으면 이름이 다르더라도 생성이된다
하지만 필드 주입이나, Setter 주입의 경우 별칭이 중요하기 때문에 이름을 맞춰줘야한다.

  • 프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안
  • username.as("memberName") : 필드에 별칭 적용
    • .as("별칭")
  • ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
    • ExpressionUtils.as(source,alias)
//field 주입 as 사용
@DisplayName("findUserDtoByField")
    @Test
    void findUserDtoByField() {
        List<UserDto> dtoList = jpaQueryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        member.age))
                .from(member)
                .fetch();
    }
//field 주입 ExpressionUtils.as 사용

@DisplayName("findUserDto")
    @Test
    void findUserDto() {
        QMember memberSub = new QMember("memberSub");
        List<UserDto> dtoList = jpaQueryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        ExpressionUtils.as(//서브 쿼리
                                JPAExpressions
                                .select(memberSub.age.max())
                                        .from(memberSub), "age")
                ))
                .from(member)
                .fetch();
    }

3) 생성자 @QeuryProjection

  • 생성자 호출이랑 비슷하지만 QeuryProjection은 컴파일시 타입체크 등의 에러를 발생시켜 준다.
  1. 생성자에 @QueryProjection 어노테이션을 달아준다
@ToString
@Getter
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  1. Gradle에서 CompileQuerydsl 클릭
    그러면 DTO도 Q파일로 생성된다.

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

문제점

  • Q파일을 생성해 줘야한다.
  • DTO는 기존에는 Querydsl에 대한 라이브러리 의존성이 없었는데, 이렇게 될 경우 ex) Member DTO가 Querydsl에 의존성을 갖게 된다
    그래서만약 Querydsl 라이브러리를 뺄 경우 사용한 곳에서다 다 에러가 발생

무조건 사용하면 좋지 않다는 것 보다는 어플리케이션 전반적으로 이미 Querydsl에 의존하고 있거나 앞으로를 봤을 때 querydsl을 많이 사용할 것 같다면 QeuryProjection 방법을 사용하는 것도 좋음.

profile
하루 일지 보단 행동 고찰 과정에 대한 개발 블로그

0개의 댓글