[QueryDSL] DTO

KMS·2022년 5월 10일
0

QueryDSL

목록 보기
7/8

QueryDSL에서 DTO로 변환해서 값을 반환하는 것을 알아보도록 하겠습니다.

DTO(MemberDTO)

@Data
@ToString(of = {"id", "username", "age"})
public class MemberDTO {

    private Long id;
    private String username;
    private int age;
    private Team team;

    public MemberDTO() {

    }

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

Method #1: Setter

	@Test
    void findDtoBySetter() {
        //Setter로 값을 가져옴
        List<MemberDTO> result = factory
                .select(Projections
                        .bean(MemberDTO.class, member.username, member.age))
                .from(member)
                .fetch();

        Assertions.assertThat(result).extracting("username", "age")
                .containsExactly(new Tuple("MemberA", 25),
                        new Tuple("MemberB", 35),
                        new Tuple("MemberC", 45),
                        new Tuple("MemberD", 55));
    }

DTO에서의 Setter을 사용해서 값들을 주입해주는 방식입니다. SELECT에서 Projections.bean(DTO.class, parameter...) 을 이용해서 원하는 DTO에 원하는 값들을 주입합니다. 이때, 값을 가져오는 엔티티에서의 속성 이름이랑 DTO에서의 속성 이름이 같아서 매칭되는 속성에 값들이 주입되는 점을 주의하시면 됩니다. 또한, DTO에 Setter가 있어야 작동하는 점도 주의하시면 됩니다.

Method #2: Field

	@Test
    void findDtoByField() {
        //값들을 바로 속성 값으로 넣어줌
        List<MemberDTO> result = factory
                .select(Projections
                        .fields(MemberDTO.class, member.username, member.age))
                .from(member)
                .fetch();


        Assertions.assertThat(result).extracting("username", "age")
                .containsExactly(new Tuple("MemberA", 25),
                        new Tuple("MemberB", 35),
                        new Tuple("MemberC", 45),
                        new Tuple("MemberD", 55));
    }

DTO의 Field에 값들 바로 주입하는 방식입니다. SELECT에서 Projections.fields(DTO.class, parameter...) 을 이용해서 원하는 DTO에 원하는 값들을 주입합니다. 이때, 값을 가져오는 엔티티에서의 속성 이름이랑 DTO에서의 속성 이름이 같아서 매칭되는 속성에 값들이 주입되는 점을 주의하시면 됩니다.

Method #3: Constructor

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

        Assertions.assertThat(result).extracting("username", "age")
                .containsExactly(new Tuple("MemberA", 25),
                        new Tuple("MemberB", 35),
                        new Tuple("MemberC", 45),
                        new Tuple("MemberD", 55));
    }

Projections.constructor(DTO.class, parameters...) 를 통해 DTO의 생성자들을 통해서 값들을 주입하는 방식입니다. 이 방식은 순수 JPA에서 DTO로 값들을 반환할때랑 비슷한 방식입니다. 해당 방식을 사용할 경우에는, 목적에 맞는 생정자가 필요한 점을 주의하시면 됩니다.

Entity랑 DTO의 속성 값이 다를때는?

첫번째 방식과 두번째 방식은 Entity의 속성 값과 DTO에서의 속성 값이 같을때 사용 가능합니다. 다를 경우에는 오류가 발생하는데, 다를 경우의 해결방법을 보도록 하겠습니다.

DTO(UserDTO)

@Data
public class UserDTO {

    private String name;
    private int age;

    public UserDTO() {
    }

    public UserDTO(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Entity에서의 username이 UserDTO는 name으로 변경 됐습니다. username을 name에 넣는 방식을 보도록 하겠습니다.

Solution:

	@Test
    void findByUserDto() {
        List<UserDTO> result = factory
                .select(Projections.fields(UserDTO.class, member.username.as("name"), member.age))
                .from(member)
                .fetch();

        Assertions.assertThat(result).extracting("name", "age")
                .containsExactly(new Tuple("MemberA", 25),
                        new Tuple("MemberB", 35),
                        new Tuple("MemberC", 45),
                        new Tuple("MemberD", 55));

    }

별칭을 지정해 주는 as(alias)을 이용해서 오류를 해결합니다. 만약에 as(alias)로 별칭을 지정해 주지 않으면, UserDTO에는 username과 매칭되는 속성 값이 없기 때문에 name에는 null 값이 저장됩니다. 그렇기 때문에, as(alias)를 이용해서 member.username이 UserDTO의 name과 매칭된다는 것을 알려줍니다.(member.username.as("name")).
또 다른 해결방안은 세번째 방식이였던 생성자를 통해 속성 값들을 주입시켜주는 방식입니다.

+ Subquery

	@Test
    void findByUserDtoSubQueryAlias() {
        QMember subMember = new QMember("sub");

        List<UserDTO> result = factory
                .select(Projections.fields(UserDTO.class,
                        member.username.as("name"),
                        ExpressionUtils.as(JPAExpressions
                                .select(subMember.age.max())
                                .from(subMember), "age")))
                .from(member)
                .fetch();

        Assertions.assertThat(result).extracting("name", "age")
                .containsExactly(new Tuple("MemberA", 55),
                        new Tuple("MemberB", 55),
                        new Tuple("MemberC", 55),
                        new Tuple("MemberD", 55));

    }

해당 예제는 member.username -> UserDTO.name, 그리고 member.age의 최댓값 -> UserDTO.age 로 변환하는 방법입니다. 여기서 눈 여겨 볼 것은 member.age의 최댓값을 서브쿼리로 가져와야 되는데, 서브쿼리로 받은 값에도 별칭을 지정해주는 것 입니다.
ExpressionUtils.as(JPAExpressions.select(subMember.age.max()).from(subMember), "age")))
서브쿼리를 이용해서 최댓값을 가져오는 것은 앞서 서브쿼리에서 봤던 것과 같은데, 여기서 "age" 로 별칭을 지정해 줄 수 있다는 것이 중요한 부분입니다.

profile
Student at Sejong University Department of Software

0개의 댓글