스프링데이터 + JPA/QueryDSL

21. 프로젝션 Dto

sdafdq 2023. 12. 1. 08:15

QueryDSL로 프로젝션으로 Dto를 직접 받는 방법은 크게 3가지가 있다. 직접 받는다기 보다는 바로 받는다. 가 맞는 것 같다.

setter가 활용되는 bean방식

필드에 직접 주입해주는 fields 방식

생성자 방식

 

먼저 bean 방식부터.

 

그 전에, jpql방식

@Test
public void findDtoBytJPQL(){
    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);
    }
}

저렇게 패키지 명까지 써 줘야 했던게 안타까웠다.

 

하지만 QueryDSL에선,

 

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

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

이렇게.

Projections란 QueryDSL에서 제공하는 프로젝션에 관한 static 메소드를 가지고 있는 클래스 이다. 

 

필드 방식과 bean 방식은 QBean이라는 것을 반환한다.

 

 

Projections.bean(클래스타입, 데이터, 데이터, ...)

클래스 타입을 넣고, 그 뒤에 데이터 타입들을 넣는데, 저 때 중요한 점이, setter명과 맞아야 한다.

즉, m.username이므로, setter이름이 setUsername 이어야 한다.

 

만약 setter의 이름이 다르다면,

@Test
public void findDtoBySetter(){
    List<MemberDto> result = query.select(Projections.bean(MemberDto.class, m.username.as("name"), m.age))
            .from(m)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

저렇게 as("별칭") 해서 name으로써 가져올 수 있다. 그러면 setName()을 호출하게 된다. (만약, setter가 없을 경우는 알아서 에러를 먹는건지 확인을 해보는 건지, 그냥 안 받은 필드가 있다면 null로 처리하고 넘어간다.

 

그리고, 빈 기본 생성자가 있어야 한다. setter를 활용하는 거니 빈 생성자로 만들고, setter로 넣어주는 거다.

 

/* select
    member1.username as name,
    member1.age 
from
    Member member1 */ select
        m1_0.username,
        m1_0.age 
    from
        member m1_0

별칭 사용할 경우 저렇게 as 로 된다. sql에서는 없는 걸 보니, JPA에서만 따로 처리하는 듯 하다.

 

memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)

 

잘 들어간다.

 

그 다음 필드에 직접 주입 시키는 거.

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

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

Projections.fields(클래스타입, 데이터 .....)

똑같다.

 

동작 방식은 필드에 직접 주입시킨다고 하는데,

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

이 상태로 잘 들어간다.

 

분명 필드가 private인데 그런데도 주입이 된다.

 

이거는 라이브러리에서 뭐 따로 하는 게 있다고 ..

리플렉션을 통한 접근을 한다고 한다.

리플렉션이란 객체의 구체적인 타입을 알지 못해도 해당 객체의 멤버변수, 메서드 등에 접근할 수 있도록 해주는 자바의 기능이라고 한다.

 

private 이라도 접근이 가능하다는 거다. private, public 이런 거 몰라도 해당 객체의 멤버변수, 메서드 등에 접근할 수 있다.

 

하긴 private가 유지보수를 위해 막는거지 정말로 외부에서 접근하면 안돼! 하고 막는 것이 아니다.

 

근데 리플렉션 방식이라도 빈 생성자는 필요하다.

 

/* select
    member1.username,
    member1.age 
from
    Member member1 */ select
        m1_0.username,
        m1_0.age 
    from
        member m1_0

 

memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)

 

 

 

다음 생성자 주입

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

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

이것도 사용방법은 똑같다.

Projections.constructor(MemberDto.class, 데이터 ...)

아무래도 생성자 접근 방식이다 보니 해당 생성자가 있어야 하며, 당연히 데이터의 순서도 맞아야 한다.

정말로 생성자를 찾아서, 그 생성자로 만드는 것이다.

타입, 순서가 중요하고, 이름은 꼭 중요하지는 않다.

 

/* select
    member1.username,
    member1.age 
from
    Member member1 */ select
        m1_0.username,
        m1_0.age 
    from
        member m1_0

 

memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)

 

 

@Test
public void findUserDto(){
    List<UserDto> result = query.select(Projections.fields(UserDto.class, m.username.as("name"), m.age))
            .from(m)
            .fetch();

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

필드, bean 방식일 때 이름이 안 맞는 경우,

위에서 했다.

as 해서 별칭을 붙여서 바꿔서 가져오면 된다.

/* select
    member1.username as name,
    member1.age 
from
    Member member1 */ select
        m1_0.username,
        m1_0.age 
    from
        member m1_0

 

memberDto = UserDto(name=member1, age=10)
memberDto = UserDto(name=member2, age=20)
memberDto = UserDto(name=member3, age=30)
memberDto = UserDto(name=member4, age=40)

 

 

 

 

 

이제 서브쿼리를 사용하였을 때 그걸 어떻게 Dto로 넣는지 살펴보자.

@Test
public void findUserDtoSub(){
    QMember mSub = new QMember("mSub");
    List<UserDto> result = query.select(
                    Projections.fields(
                    	UserDto.class, m.username.as("name"),
                    	ExpressionUtils.as(JPAExpressions.select(mSub.age.max()).from(mSub), "age")
                    )
            )
            .from(m)
            .fetch();

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

 

ExpressionUtils라는 것이 있다.

저것도 QueryDSL꺼다. 표현식 유틸이다. 기본적으로 Q클래스들은 대상 엔티티에 대한 표현식의 모음이라고 생각하면 된다. 즉, m.username 이런 거 다 값보다는 표현식이라고 생각하면 된다.

 

ExpressionUtils는 말 그대로 표현식 유틸이다. 표현식 도구이다.

as는 위에 썼던 것 처럼 별칭, 무언가의 표현식에 별칭을 붙여주는 메소드이다. 
저 ExpressionUtils.as()의 return 타입은 표현식이다.

 

그렇게 Projection.fields, 즉 프로젝션 안에 as name과 as age 두개의 표현식이 들어간 것이다.

 

 

 

이런 식인 셈.

 

 

@Test
public void findUserDtoConstructor(){
    List<UserDto> result = query.select(Projections.constructor(UserDto.class, m.username, m.age))
            .from(m)
            .fetch();

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

마지막으로 다시 constructor 방식.

앞서 말 했듯 필드명과 맞출 필요가 없다. 그냥 타입, 순서만 잘 맞추고 해당 Dto에 그 생성자가 있어야 한다.

'스프링데이터 + JPA > QueryDSL' 카테고리의 다른 글

23. 동적쿼리. BooleanBuilder 방식  (0) 2023.12.01
22. @QueryProjection  (0) 2023.12.01
20. 프로젝션  (0) 2023.11.30
19. 상수, 문자 더하기  (0) 2023.11.30
18. CASE 문  (0) 2023.11.30