본문 바로가기
Develop/Spring

[querydsl] querydsl에서 projection 다루기

by 코딩의성지 2021. 7. 7.

db를 다루다 보면 종종 projection을 다뤄야할 때가 있다.

 

혹시나 Projection이 무엇인지를 잘 모르는 분들을 위해 간단하게 Projection의 개념에 대해 정리를 해두겠다.

 

Projection 연산

- 한 Relation의 Attribute들의 부분 집합을 구하는 연산자

- 결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가짐

- 결과 Relation은 기본 키가 아닌 Attribute 에 대해서만 중복된 tuple들이 존재할수 있음

 

대충 이런건데.. 이를 querydsl에서도 구현할 수 있다.

 

만약에 Projection의 대상이 하나인 경우에는 그냥 쉽게 아래 예제처럼 하면된다.

List<String> result = queryFactory
		.select(member.userName)
		.from(member)
		.fetch();

 

하지만 Projection 대상이 하나 보다 더 많은 경우는 아래처럼 리턴값이 Tuple이라는 타입으로 리턴된다.

List<Tuple> result = queryFactory
		.select(member.userName, member.age)
		.from(member)
		.fetch();

하지만 이 tuple은 model 객체를 로직에서 사용하는 것과 같은 문제를 가지고 있기에, 최대한 사용을 피하고 쓰려면 repository 레벨에서만 쓰기를 권장한다.

 

그래서 우리는 DTO(Data Transfer Object) 라는걸 만들어 쓰는데,

JPQL에서는 아래와 같은 방법으로 사용하면된다.

package study.querydsl.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

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

    public MemberDto(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }
}
List<MemberDto> result = em.createQuery(
				"select " +
				"new study.querydsl.dto.MemberDto(m.userName, m.age) "+
				"from Member m", MemberDto.class)
		.getResultList();

그런데 이방법은 치명적인 단점이 있는데.... query를 작성하다가 오타를 낼 엄청난 리스크가 있다.

 

그래서 이를 querydsl로 바꿔보면 세가지 방법으로 바꿀수 있다.

 

1. 생성자 조회

package study.querydsl.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class TeamMemberDto {
    private String teamMemberName;
    private int age;

    public TeamMemberDto(String teamMemberName, int age) {
        this.teamMemberName = teamMemberName;
        this.age = age;
    }
}
// 생성자의 경우 type만 맞으면됨
List<TeamMemberDto> result = queryFactory
//		.select(Projections.constructor(MemberDto.class,
//				member.userName, member.age)) //
		.select(Projections.constructor(TeamMemberDto.class,
				member.userName, member.age)) //
		.from(member)
		.fetch();

생성자의 경우 타입만 맞으면 된다. TeamMemberDto는 teamMemberName이 Member 도메인의 userName과 매칭될수 있는 이유가 타입이 맞기 때문이다.

 

2. Setter 조회

setter 메서드를 이용해서 조회하는 방법도 있는데 아래의 예시를 따르면된다.

setter 메서드를 이용할때는 이렇게 dto의 멤버변수들과 entity의 멤버 변수가 일치해야한다.

List<MemberDto> result = queryFactory
		.select(Projections.bean(MemberDto.class,
				member.userName, member.age)) // 이게 dto 랑 정확히 매칭 되어야 쓸수 있음
		.from(member)
		.fetch();

 

3. Field 직접 조회

field를 직접 조회해서 사용할 수도 있다.

setter와 동일하게 멤버변수들의 명칭일 일치해야한다.

List<MemberDto> result = queryFactory
		.select(Projections.fields(MemberDto.class,
				member.userName, member.age)) // 이게 dto 랑 정확히 매칭 되어야 쓸수 있음
		.from(member)
		.fetch();

 

4. as

그리고 아래의 예처럼 as 메서드를 이용해서 alias를 정할수 있고, 또 서브쿼리를 작성할때도 alias를 정해서 만들어 낼수도 있다.

QMember subMember = new QMember("subMember");

List<TeamMemberDto> result = queryFactory
		.select(Projections.fields(TeamMemberDto.class,
				member.userName.as("teamMemberName"), // 아래와 같은 표현
				//ExpressionUtils.as(member.userName, "teamMemberName"),
				ExpressionUtils.as(
						JPAExpressions
							.select(subMember.age.max())
							.from(subMember), "age")
		))
		.from(member)
		.fetch();

 

 

그리고 또 하나 더 작성하자면 dto도 Q 파일로 만들어서 사용할수 가 있다.

만들고자하는 생성자에 @QueryProjection을 달아주고 , compileQuerydsl 로 컴파일해주면 Q 파일이 생긴다.

package study.querydsl.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;

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

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

 

그리고 이젠 아래처럼 이렇게 쉽게 쓸 수 있다.

List<MemberDto> result = queryFactory
		.select(new QMemberDto(member.userName, member.age))
		.from(member)
		.fetch();

이렇게 하면 오류 발생시 런타임이 아닌 컴파일 타임에서 잡을수있다는 엄청난 장점도 있는 반면, 지나치게 QueryDSL에 의존적이라는 설계적인 관점에서는 단점이 존재한다.

 

반응형

댓글