본문 바로가기
JPA/Querydsl

Querydsl 07 - 프로젝션과 결과반환

by 킹차니 2022. 1. 19.

프로젝션과 결과 반환 - 기본

프로젝션: select 대상 지정

 

먼저 MemberDto는 아래와 같다.

@NoArgsConstructor
@Data
public class MemberDto {
    private String username;
    private int age;

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

 

 

 

프로젝션 대상이 하나인 경우

@Test
void simpleProjection(){
    List<String> result = queryFactory.select(member.username).from(member).fetch();
    for (String username : result) System.out.println("username = " + username);

    List<Member> result2 = queryFactory.select(member).from(member).fetch();
    for (Member m : result2) System.out.println("m = " + m);
}

출력결과:
username = member5
username = member1
username = member2
username = member3
username = member4

m = Member(id=3, username=member5, age=55)
m = Member(id=4, username=member1, age=10)
m = Member(id=5, username=member2, age=20)
m = Member(id=6, username=member3, age=10)
m = Member(id=7, username=member4, age=20)

프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.

• 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회

 

 

 

튜플 조회

프로젝션 대상이 둘 이상일 때 사용

@Test
void tupleProjection() {
    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("-----------");
        System.out.println(username);
        System.out.println(age);
    }
}

출력결과:
-----------
member5
55
-----------
member1
10
-----------
member2
20
-----------
member3
10
-----------
member4
20

위와 같은 Tuple은 querydsl이 제공한다. 하여 리퍼지토리의 앞단인 컨트롤러, 서비스 계층에서는 사용하지 말자.

 


 

프로젝션과 결과 반환 - DTO조회

 

 

순수 JPA에서 DTO조회:

@Test
void findDtoByJPQL() {
    List<MemberDto> resultList =
            em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
                    .getResultList();
    for (MemberDto memberDto : resultList) System.out.println("memberDto = " + memberDto);
}

출력결과:
memberDto = MemberDto(username=member5, age=55)
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=10)
memberDto = MemberDto(username=member4, age=20)

순수 JPA에서는 DTO를 조회할 때 new 명령어를 사용하고, DTO의 패키지를 모두 적어야 해서 매우 지저분하다. 또한 생성자 방식만 제공한다.

 

 

Querydsl 빈 생성(Bean population)

결과를 Dto 반환할 때 사용한다. 다음 3가지 방법을 지원한다.

• 프로퍼티 접근(setter)

• 필드 직접 접근(필드 set)

• 생성자 사용(constructor)

 

1. 프로퍼티 접근 

@Test
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);
}

출력결과:
memberDto = MemberDto(username=member5, age=55)
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=10)
memberDto = MemberDto(username=member4, age=20)

• 위의 방법을 사용하기 위해 Dto 클래스에 기본 생성자가 있어야 한다.

• select 절에 Projections.bean(반환dto클래스, set하고 싶은 필드1, set하고 싶은 필드2, ...)을 넣어준다.

 

 

 

2. 필드 직접 접근

@Test
void findDtoByFields() {
    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);
}
//출력결과 위와 같음

• select 절에 Projections.fields(반환dto클래스, 필드1, 필드2, ...)을 넣어준다.

 

 

 

3. 생성자 접근

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

    for (MemberDto memberDto : result) System.out.println("memberDto = " + memberDto);
}
//출력결과 위와 같음

• select 절에 Projections.constructor(반환dto클래스, 필드1, 필드2, ...)을 넣어준다.

 다만 이때 Dto에 존재하는 생성자의 순서와 constructor안에 들어가는 필드들의 순서가 같아야 한다. 또한 필드명이 일치해야 한다.(프로퍼티, 필드, 생성자 접근 방법 모두 해당)

 

 

만약 필드명이 일치하지 않는 경우에는 as를 사용하여 필드명을 맞춰주자.

필드명이 일치하지 않는 예르 보기 위해 UserDto를 정의하였다.

@NoArgsConstructor
@Data
public class UserDto {
    private String name;
    private int age;

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

그리고 아래의 select절의 member.username.as("name")으로 해준다.

@Test
void findUserDto1() {
    List<UserDto> result = queryFactory
            .select(Projections.constructor(UserDto.class, member.username.as("name"), member.age))
            .from(member)
            .fetch();

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

 

 

또한 서브쿼리를 사용하여 별칭을 부여할 때는 ExpressionUtils를 사용한다.

@Test
void findUserDto3() {

    QMember memberSub = new QMember("memberSub");
    List<UserDto> result = queryFactory
            .select(Projections
                    .constructor(
                            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);
}

 


 

 

프로젝션과 결과 반환 - @QueryProjection

생성자 + @QueryProjection

 

Dto클래스에 @QueryProkection 어노테이션을 붙여주는 방법이다. 해당 어노테이션을 추가하고, gradle -> Tasks -> other -> compileQuerydsl을 더블클릭하면 해당 Dto클래스에 대한 Q타입을 만들어 준다.

앞서 만든 MemberDto의 Q타입을 만들었다.

/**
 * study.querydsl.dto.QMemberDto is a Querydsl Projection type for MemberDto
 */
@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QMemberDto extends ConstructorExpression<MemberDto> {

    private static final long serialVersionUID = 1356709634L;

    public QMemberDto(com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<Integer> age) {
        super(MemberDto.class, new Class<?>[]{String.class, int.class}, username, age);
    }

}

QMemberDto를 보면 public생성자가 보인다. 해당 생성자를 사용하는 것이다.

 

테스트해보자.

@Test
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);
}

출력결과:
memberDto = MemberDto(username=member5, age=55)
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=10)
memberDto = MemberDto(username=member4, age=20)

위와 같은 방법은 QMemberDto의 생성자를 사용하기 때문에 아래와 같이 없는 필드를 추가하거나 그에 맞는 생성자가 없는 경우에는 컴파일 에러를 발생시킨다. 이것이 매우 큰 장점이다.

@Test
void findDtoByQueryProjection() {
    List<MemberDto> result = queryFactory
            .select(new QMemberDto(member.username, member.age, member,id))//컴파일 에러 발생
            .from(member)
            .fetch();
    for (MemberDto memberDto : result) System.out.println("memberDto = " + memberDto);
}

 

단 한가지 단점이 있다.

MemberDto에 @QueryProjections 어노테이션을 붙이기 때문에 MemberDto querydsl의 라이브러리에 대한 의존성을 가지게 된다는 것이다.

 

 


 

추가로 아래와 같이 distinct를 사용할 수 있다.

@Test
void distinct() {
    List<String> result = queryFactory
            .select(member.username).distinct()
            .from(member)
            .fetch();
}

 

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.

 

'JPA > Querydsl' 카테고리의 다른 글

Querydsl 09 - 수정, 삭제 배치 쿼리  (0) 2022.01.20
Querydsl 08 - 동적 쿼리  (0) 2022.01.20
Querydsl 06 - Case문, 상수 문자 더하기  (0) 2022.01.18
Querydsl 05 - 서브쿼리  (0) 2022.01.18
Querydsl 04 - 조인  (0) 2022.01.17