Querydsl - 프로젝션

HotFried·2024년 2월 26일
0

Querydsl

목록 보기
7/9

프로젝션이란?
-> select할 대상을 찾는 것

프로젝션 대상이 하나일 때

class Projection {
    void example() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();
    }
}

-> String과 같이 타입을 명확하게 지정할 수 있다.


프로젝션 대상이 여러개일 때

class Projection {
    void example() {
        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("username=" + username);
            System.out.println("age=" + age);
        }
    }
}

-> 결과가 여러 개일 때 담을 수 있도록 만든 Querydsl 객체인 Tuple을 반환한다.
-> Tuple 또는 Dto를 반환하도록 한다.


Tuple말고 Dto반환하기 - 순수JPA

먼저 반환할 Dto 엔티티를 생성해준다.

MemberDto

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

    public MemberDto() {
    }

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • 엔티티로 가져오면 필요하지 않은 데이터도 다 가져와야 하므로 필요한 데이터만 가져올 수 있게 DTO를 만든다.
  • Querydsl이 MemberDto객체를 생성한 후 작업하기 때문에 MemberDto에 기본 생성자가 꼭 필요하다.

    이때 기본 생성자의 접근제한자는 public으로 선언하도록 한다.
    Entity의 기본 생성자를 만들 때는, Proxy의 특징으로 인해 Protected까지 생성이 가능하지만
    <엔티티(Entity) 기본 생성자 - Reflection API 참고>

    Dto의 경우 다른 패키지에서 이용할 수 있기 때문에 반드시 public으로 설정한다.
    그렇지 않으면 IllegalAccessException런타임에러가 발생한다.

순수 JPA에서 Dto를 반환할 때는 패키지명까지 전부 기술해야하고 생성자 방식만 지원한다.

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

Dto 반환하기 - querydsl 빈 생성

프로퍼티 접근

beansetter로 필드에 정보를 주입해주는 방식이다.

    @Test
    void findDtoBySetter() {
        List<MemberDto> result = queryFactory
                // bean이 setter로 주입해준다.
                .select(Projections.bean(
                        MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }

필드 접근

fields메소드를 사용한다.
querydsl이 Dto의 필드에 변수를 바로 꽂아버린다.

	@Test
    void findDtoByField() {
        List<MemberDto> result = queryFactory
                // 필드에 바로 넣는다.
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }

생성자 사용

constructor 메소드를 사용한다.
생성자의 파라미터 변수로 Qclass의 필드를 이용한다.

	@Test
    void findDtoByConstructor() {
        List<MemberDto> result = queryFactory
                // 생성자를 사용한다.
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();
    }

Dto내의 필드 별칭이 다를 때

QMember클래스의 이름 필드는 username이다.
하지만 아래의 UserDto의 이름 필드는 name이다

그리고 나이는 member들의 최대치로 고정시키고 싶다... 어떻게 해야 좋을까?

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

먼저 필드의 이름이 다르다면 alias를 이용할 수 있다.
member.username.as("name")코드처럼 alias를 이용해 문제를 해결할 수 있다.

나이의 경우 ExpressionUtils를 이용해 서브쿼리로 작성을 해주고 alias를 이용해 이름을 맞춰주면 된다.

QMember memberSub = new QMember("memberSub");

List<UserDto> result = queryFactory
        .select(Projections.fields(UserDto.class,
                member.username.as("name"),

                ExpressionUtils.as(JPAExpressions
                        .select(memberSub.age.max())
                        .from(memberSub), "age")
        ))
        .from(member)
        .fetch();

@QueryProjection

생성자에 @QueryProjection 어노테이션을 붙인뒤 compile을 해주면 별도의 QClass가 생성된다.

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

    public MemberDto() {
    }

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

컴파일러로 타입을 체크할 수 있기 때문에 런타임에러를 체크해야했던 다른 방법과 달리 가장 안전한 방법이다.
하지만, DTO에 QueryDSL 어노테이션을 유지해야 하는 점 & DTO까지 Q 파일을 생성해야 하는 단점이 있다.
-> 엔티티가 Querydsl에 의존적이게 되는 단점도 존재한다.

@Test
void findDtoByQueryProjection() {
    List<MemberDto> result = queryFactory
            // DTO 클래스를 new로 바로 넣으면 된다.
            // 생성자로 만들기 때문에 타입과 파라미터를 다 맞춰줘서 안정적으로 만들 수 있다.
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();
    }

참고 :

실전! Querydsl

profile
꾸준하게

0개의 댓글