-> 블로그 이전

[QueryDSL] 프로젝션, DTO 조회

2022. 8. 28. 14:47Language`/JPA

프로젝션

프로젝션이란 "Select절에 조회 대상을 지정"하는 것이다

 

대상 하나

조회대상이 하나라면 Return Type은 해당 조회 대상의 Type으로 정해진다

// Projection [Member]
List<Member> fetch1 = query.select(member)
        .from(member)
        .where(member.age.goe(25))
        .fetch();

// Projection [String]
List<String> fetch2 = query.select(member.username)
        .from(member)
        .where(member.age.goe(25))
        .fetch();

// Projection [Integer]
List<Integer> fetch3 = query.select(member.age)
        .from(member)
        .where(member.age.goe(25))
        .fetch();

 

대상 여러개 (Tuple)

QueryDSL에서는 프로젝션 대상으로 여러 필드를 선택하게 되면 Tuple이라는 "Map과 비슷한" Type을 return해준다

List<Tuple> fetch = query.select(member.username, member.age)
        .from(member)
        .fetch();

for (Tuple tuple : fetch) {
    System.out.println("Name = " + tuple.get(member.username) + " -> Age = " + tuple.get(member.age));
}

 

빈 생성

쿼리에 대한 결과를 엔티티가 아닌 "특정 객체(DTO..)"로 받고 싶다면 Bean Population을 사용한다

 

1) 프로퍼티 접근

프로퍼티 접근은 결과에 대한 DTO에 "필드의 getter/setter"를 생성하고 Projections.bean()을 통해서 결과를 받아오면 된다

  • 첫번째 파라미터 = 결과에 대한 클래스 (ItemDto.class)
  • 두번째/세번째/.... 파라미터 = 매핑될 필드

Projections.bean()은 "Setter"를 사용해서 값을 채우게 된다

그리고 파라미터가 없는 생성자(access = Public)가 "반드시" 필요하다

// MemberOrderDto
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberOrderDto {
    private Long memberId;
    private Long orderId;
    private String memberName;
    private Integer memberAge;
    private String orderProductName;
    private Integer orderAmount;
}
List<MemberOrderDto> fetch = query.select(
                Projections.bean(
                        MemberOrderDto.class,
                        order.member.id.as("memberId"),
                        order.id.as("orderId"),
                        order.member.username.as("memberName"),
                        order.member.age.as("memberAge"),
                        order.product.name.as("orderProductName"),
                        order.orderAmount.as("orderAmount")
                )
        ).from(order)
        .innerJoin(order.member)
        .innerJoin(order.product)
        .fetch();

for (MemberOrderDto memberOrderDto : fetch) {
    System.out.println(memberOrderDto);
}

>> 쿼리 결과 ↔ 매핑할 DTO의 프로퍼티명이 다르다면 "반드시" as를 통해서 별칭을 설정해줘야 한다

 

2) 필드 직접 접근

필드 직접 접근은 Projections.fileds()를 통해서 "필드에 직접 접근"해서 값을 채워준다

  • 여기서 필드를 private로 설정해도 제대로 동작한다

그리고 파라미터가 없는 생성자(access = Public)가 "반드시" 필요하다

List<MemberOrderDto> fetch = query.select(
                Projections.fields(
                        MemberOrderDto.class,
                        order.member.id.as("memberId"),
                        order.id.as("orderId"),
                        order.member.username.as("memberName"),
                        order.member.age.as("memberAge"),
                        order.product.name.as("orderProductName"),
                        order.orderAmount.as("orderAmount")
                )
        ).from(order)
        .innerJoin(order.member)
        .innerJoin(order.product)
        .fetch();

for (MemberOrderDto memberOrderDto : fetch) {
    System.out.println(memberOrderDto);
}

 

3) 생성자 사용

생성자를 사용한 DTO조회는 Projections.constructor()를 통해서 DTO의 생성자를 사용해서 값을 return하게 된다

  • 여기서 지정한 프로젝션과 생성자의 파라미터 순서는 "정확하게" 일치해야한다
  • 파라미터가 없는 생성자가 없어도 "필드에 대한 생성자"만 정확하게 있으면 동작한다
List<MemberOrderDto> fetch = query.select(
                Projections.constructor(
                        MemberOrderDto.class,
                        order.member.id.as("memberId"),
                        order.id.as("orderId"),
                        order.member.username.as("memberName"),
                        order.member.age.as("memberAge"),
                        order.product.name.as("orderProductName"),
                        order.orderAmount.as("orderAmount")
                )
        ).from(order)
        .innerJoin(order.member)
        .innerJoin(order.product)
        .fetch();

for (MemberOrderDto memberOrderDto : fetch) {
    System.out.println(memberOrderDto);
}

 

@QueryProjection

DTO클래스의 생성자 레벨에 @QueryProjection 애노테이션을 붙이게 되면 DTO 클래스도 QType이 생성되고 이를 그대로 결과로 매핑시켜서 받아오면 된다

  • 기본적인 Projections을 통해서 DTO 조회보다 TypeSafe하지만, 물론 QueryDSL에 더 종속적이다는 단점도 존재한다
@Data
public class MemberOrderDto {
    private Long memberId;
    private Long orderId;
    private String memberName;
    private Integer memberAge;
    private String orderProductName;
    private Integer orderAmount;

    @QueryProjection
    public MemberOrderDto(Long memberId, Long orderId, String memberName, Integer memberAge, String orderProductName, Integer orderAmount) {
        this.memberId = memberId;
        this.orderId = orderId;
        this.memberName = memberName;
        this.memberAge = memberAge;
        this.orderProductName = orderProductName;
        this.orderAmount = orderAmount;
    }
}
List<MemberOrderDto> fetch = query.select(
                new QMemberOrderDto(
                        order.member.id,
                        order.id,
                        order.member.username,
                        order.member.age,
                        order.product.name,
                        order.orderAmount
                )
        ).from(order)
        .innerJoin(order.member)
        .innerJoin(order.product)
        .fetch();

for (MemberOrderDto memberOrderDto : fetch) {
    System.out.println(memberOrderDto);
}

  • as를 통해서 프로젝션 필드에 대한 Alias를 붙여주지 않아도 DTO QType이 알아서 번역해서 제대로 쿼리가 들어감을 확인할 수 있다