[Querydsl] 16. 프로젝션과 결과 반환

민정·2023년 1월 9일

QueryDSL

목록 보기
16/18
post-thumbnail

📌 프로젝션이란?

select절에 대상을 지정하는 것

  • 프로젝션 대상이 하나 : 반환 타입을 명확히 지정 가능
  • 프로젝션 대상이 둘 이상 : 튜플이나 DTO로 조회


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

1. 프로젝션 대상이 하나

  • 프로젝션 대상 : member.username - 당연히 반환 타입 명확하게 String
  • 프로젝션 대상 : member - 당연히 반환 타입 명확하게 Member
 	/**
     * 프로젝션 - 프로젝션 대상이 하나
     */
    @Test
    public void simpleProjection() throws Exception {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();

        // member 객체 조회 -> 이것도 프로젝션 대상이 하나(반환 타입 명확하게 지정 Member라고 가능)
        List<Member> result2 = queryFactory
                .select(member)
                .from(member)
                .fetch();
    }

2. 프로젝션 대상이 둘 이상 - 튜플 조회

✅ 튜플

querydsl이 여러개의 대상을 프로젝션할 때를 대비해서 만든 타입
com.querydsl.core.Tuple

✅ querydsl에서 제공하는 Tuple 타입은 repository 계층까지만 사용 권장

튜플을 repository 계층 안에서 사용하는 것은 OK.
근데 service, controller 계층까지 넘어가는 것은 좋은 설계가 아님.
핵심 비즈니스 로직이나 서비스 계층이 우리가 어떤 하부 구현 기술(JPA, querydsl)을 쓰는지 아는 것은 좋지 않음.

마찬가지로 jdbc 쓸때도 jdbc가 반환해주는 resultset을 repository, dao 계층에서만 사용하고 나머지 계층에서는 이런 하부 구현 기술에 대한 의존없게 설계하는 것이 좋은 설계.
그래야 나중에 하부 구현 기술을 다른 것으로 변경하더라도 앞단(service, controller)는 변경할 것이 없음.

그래서 tuple도 repository 계층안에서만 사용하고 다른 계층으로 던질 때는, dto로 반환해서 던지는 것을 추천.

tuple이 querydsl에 종속적인 타입이라서!!

  • 프로젝션 대상 : member.username, member.age - 반환 타입 명확하게 지정 못함!

    따라서 Tuple이 반환타입으로 지정된다.
    /**
     * 프로젝션 - 프로젝션 대상이 둘 이상: 튜플 조회
     */
    @Test
    public void tupleProjection() throws Exception {
        List<Tuple> result = queryFactory
                .select(member.username, member.age) // username, age 2개 프로젝션 -> 반환 타입 지정 못하니까 Tuple
                .from(member)
                .fetch();

        for (Tuple tuple : result) {
            String username = tuple.get(member.username); // 튜플에서 값 꺼내기 : Tuple.get(Q타입.속성)
            Integer age = tuple.get(member.age);

            System.out.println("username = " + username);
            System.out.println("age = " + age);
        }
    }

✅ 결과



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

✅ Member DTO 생성

@NoArgsConstructor// querydsl DTO조회시 프로퍼티 접근 - setter 방법 때 기본 생성자 필요
@Data// @Getter, @Setter, @RequiredArgsConstructor, @ToString 등이 모두 포함
public class MemberDto {

    private String username;
    private int age;

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

✅ JPQL에서 DTO 조회

DTO가 속한 패키지 명을 풀로 다 써줘야했다!

    /**
     * 프로젝션 결과 반환 - DTO 조회
     *
     * JPQL 버전
     */
    @Test
    public void findDtoByJPQL() throws Exception {
        List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class) // select절에 MemberDto의 생성자 호출(단, 패키지 명을 풀로 작성)
                .getResultList();

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

    }


SQL 보면 username, age 딱 2개만 조회

특징

  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용
  • DTO의 패키지이름을 다 적어줘야해서 불편하다!
  • 그리고 생성자 방식만 지원한다!

✨ querydsl에서 DTO로 조회

3가지 방식을 지원한다

1. 프로퍼티 접근 - setter

주의

DTO 기본 생성자 없으면 NoSuchMethodException 발생

DTO에 반드시 있어야하는 것

setter, 기본 생성자

사용법

Projections.bean(DTO.class, DTO 필드들)

  • DTO필드들 작성 순서는 상관없다!

querydsl이 먼저 객체 생성하고나서 setter로 값을 넣어주기 때문에 기본 생성자 필요

JPQL결과와 System.out 결과는 JPA때와 같음.

    // 방법 1) 프로퍼티 접근 - Setter
    // * 기본 생성자, setter 필요
    @Test
    public void findDtoBySetter() throws Exception {
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class, // bean
                        member.username, // 순서 상관 X
                        member.age))
                .from(member)
                .fetch();

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

2. 필드 직접 접근

주의

DTO 기본 생성자 없으면 NoSuchMethodException 발생

DTO에 반드시 있어야하는 것

기본 생성자

사용법

Projections.fields(DTO.class, DTO 필드들)

  • DTO필드들 작성 순서는 상관없다!
    // 방법 2) 필드 직접 접근
    // * getter, setter 필요 X, 필드에 바로 값을 넣음.
    // * 기본 생성자는 필요
    @Test
    public void findDtoByField() throws Exception {
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class, // fields
                        member.age, // 순서 상관 X
                        member.username))
                .from(member)
                .fetch();

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

3. 생성자 사용

DTO에 반드시 있어야하는 것

생성자

사용법

Projections.constructor(DTO.class, DTO 필드들)

  • DTO필드들 작성 순서는 상관 있다! 만들어둔 생성자와 순서가 맞아야한다!
    // 방법 3) 생성자 사용
    // * 기본 생성자 필요 X, 생성자 파라미터 순서 지켜야함.
    @Test
    public void findDtoByConstructor() throws Exception {
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class, // constructor
                        member.username,// 대신 이 순서가 DTO의 생성자와 순서가 맞아야함.
                        member.age))
                .from(member)
                .fetch();

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

✨ 별칭이 다를 때(Entity - DTO간 필드명 차이)

✅ UserDto 생성

@NoArgsConstructor
@Data
public class UserDto { // MemberDto와 같은데, username 대신 name으로 필드명만 다름. -> Member Entity와도 다름.
    private String name;
    private int age;

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

✅ 별칭 부여

DTO와 Entity의 필드명이 다를 때

DTO와 Entity의 필드명이 다를 경우, setter 프로퍼티접근, 필드 직접 접근방식을 사용해 DTO를 select 해오는 경우, 별칭 부여 필요
참고!) 서브 쿼리도 별칭 부여 필요

별칭 부여 방법

1. DTO, Entity 필드명 다른 경우 - as 사용

엔티티.필드명.as("별칭:DTO 필드명")
ex) member.username.as("name")

2. 서브 쿼리의 결과 값을 DTO의 어느 필드에 넣어줘야하는지 지정 - ExpressionUtils.as(source, alias) 사용

ExpressionUtils.as(서브쿼리, "DTO 필드명")
ex) ExpressionUtils.as(select(memberSub.age.max()).from(memberSub), "age")

   /**
     * setter 프로퍼티접근, 필드 직접 접근 : DTO와 Entity의 필드명이 다를 경우 별칭 부여 필요
     *
     * + 서브 쿼리 별칭 부여 필요
     */
    @Test
    public void findUserDto() throws Exception {
        QMember memberSub = new QMember("memberSub"); // 서브쿼리용

        // 내용 1) Entity와 Dto의 필드명이 다를 경우 : as로 해결!
        // as : 필드에 별칭 적용
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,
                        //member.username, // 문제 : 프로퍼티, 필드 접근 방식에서 이름이 다를 경우 제대로 select 안됨 -> sout name = null로 나옴
                        member.username.as("name"),// 해결 : as.("DTO의 필드명") 또는 ExpressionUtils(member.username, "name")
                        member.age))
                .from(member)
                .fetch();

        // 내용 2) ExpressionUtils.as(source, alias) : 필드, 서브쿼리에 별칭 적용
        List<UserDto> result2 = queryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        // 서브쿼리 작성시, 어떤 필드에 값을 넣어줘야하는지 필드명 별칭부여 필요
                        ExpressionUtils.as(select(memberSub.age.max())
                                .from(memberSub), "age")
                ))
                .from(member)
                .fetch();

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

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

✅ 단, 생성자 사용방식을 사용해 DTO를 조회하는 경우, 데이터 타입을 보기 때문에 별칭 부여 필요 X

생성자 사용의 경우: DTO와 Entity의 필드명이 달라도, 이름이 아닌 타입을 보기 때문에 상관없다.

    /**
     * 생성자 사용의 경우: DTO와 Entity의 필드명이 달라도, 이름이 아닌 타입을 보기 때문에 상관없다.
     */
    @Test
    public void findUserDtoByConstructor() throws Exception {
        List<UserDto> result = queryFactory
                .select(Projections.constructor(UserDto.class, // constructor, UserDto
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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



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

✅ Member Dto의 생성자에 @QueryProjection + DTO Q파일 생성

Member Dto의 생성자에 @QueryProjection을 추가!

@NoArgsConstructor// querydsl DTO조회시 프로퍼티 접근 - setter 방법 때 기본 생성자 필요
@Data// @Getter, @Setter, @RequiredArgsConstructor, @ToString 등이 모두 포함
public class MemberDto {

    private String username;
    private int age;

    @QueryProjection // 생성자에 @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

compileQuerydsl 클릭
MemberDto Q파일 생성 확인

✅ @QueryProjection 사용

사용법

new QMemberDto(member.username, member.age)

    /**
     * 프로젝션과 결과 반환 - @QueryProjection
     */
    @Test
    public void findDtoByQueryProjection() throws Exception {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age)) // 컴파일러로 타입 체크 가능
                .from(member)
                .fetch();

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

        // 단점 : DTO에 QueryDsl 어노테이션 유지 + DTO까지 Q파일 생성 필요
    }

✅ 생성자 사용 방식과의 차이점

생성자 사용 방식은 코드를 잘못 작성했어도 일단 실행은 정상적으로 된다. 하지만 런타임에 오류가 발생한다.

@QueryProjection 방식은 코드를 잘못 작성하면 빨간줄이 뜬다. 컴파일 시점에 오류가 발생한다.

Querydsl이 만들어둔 생성자(= QMember 안에 정의된 함수)이미 타입이 정해져있음. 그래서 추가적으로 파라미터를 더 적거나, 잘못된 타입을 적으면 컴파일 오류가 발생한다.

장단점

장점

@QueryProjection이 컴파일 시점에 오류도 잡아주고, ctrl + p누르면 어떤 파라미터를 넣어야하는지도 나와서 가장 안전한 방법이긴 하다!

단점

  • Q파일 생성 필요
  • DTO에 @QueryProjection 어노테이션 작성으로 인해서 querydsl 라이브러리 의존성이 생김.
    • 보통 DTO은 repository, service, controller 등 여러 레이어에 걸쳐서 돌아다님.
    • 그런데 그런 DTO가 querydsl에 의존되어있음.



✨ distinct

사용법

.select().distinct()

참고

distinct는 모든 컬럼에 적용됨!
참고 블로그

    @Test
    public void distinct() throws Exception {
        List<String> result = queryFactory
                .select(member.username).distinct() // 그냥 .distinct()하면 된다, 참고로 distinct는 모든 컬럼에 적용된다.
                .from(member)
                .fetch();

    }



출처

김영한 강사님 - 인프런 실전! Querydsl

0개의 댓글