프로젝션이란 ?
QueryDsl을 이요하여 entity 전체의 값을 가져오는 것이 아닌 조회 대상을 지정해 원하는 값만 조회하는 것을 의미한다. 쉽게 말하자면 Projection이란, '테이블에서 원하는 컬럼만 뽑아서 조회하는 것 이다.
먼저 프로젝션의 대상이 하나이면 명확하게 타입을 지정할 수 있고 반환되는 타입도 명확하다.(ex: member에 대한 select를 한다면 member타입이 반환된다.)
하지만 문제는 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회해야한다
@Test
@DisplayName("튜플")
void getTuple () throws Exception {
List<Tuple> result = jpaQueryFactory
.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
String userName = tuple.get(member.username);
Integer userAge = tuple.get(member.age);
System.out.println("userName = " + userName);
System.out.println("userAge = " + userAge);
}
}
중요한 팁 : 튜플로 값을 가져오는 경우는 Querydsl에 종속적이고, Model 객체를 로직에서 사용하는 것과 같은 문제를 가지고 있기에 최대한 사용을 피하고 Repository에서만 사용하는 것이 좋다. 만약 Repository 바깥으로 나갈 땐 DTO로 변환하는 것을 권장한다. 다시 풀어서 말하자면 Repository에서 QueryDsl로 조회 후 튜플로 반환됐다면 repository에서 튜플을 -> dto로 바꿔주는 것을 권장한다.
우선 조회 응답에 필요한 MemberResponse를 제작해주겠다.
package com.spring.jpadata.dto;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
@Data
public class MemberResponse {
private String username;
private int age;
public MemberResponse() {
}
public MemberResponse(String username, int age) {
this.username = username;
this.age = age;
}
}
이제부터 3가지 방법을 소개할텐데, 셋은 공통점이 있다
Projections.XXX(반환타입,조회할 필드1,필드2,....) 의 형식을 갖을 것이다.🔽
Projections.bean
setter(bean)를 통해 데이터를 인젝션 해주며 기본 생성자가 무조건 필요하다.
@Test
@DisplayName("쿼리디에스엘 dto setter접근")
void dto() throws Exception {
//dto로 조회
List<MemberResponse> result = jpaQueryFactory.select(
Projections.bean(MemberResponse.class,
member.username,
member.age)
).from(member)
.fetch();
for (MemberResponse memberResponse : result) {
System.out.println("memberResponse = " + memberResponse);
}
}
Projections.fields
필드에 직접적으로 값을 꽂아주기 때문에 setter와 기본생성자는 필요없다.
@Test
@DisplayName("쿼리디에스엘 dto 필드접근")
void dtoByField() throws Exception {
//dto로 조회
List<MemberResponse> result = jpaQueryFactory.select(
Projections.fields(MemberResponse.class,
member.username,
member.age)
).from(member)
.fetch();
for (MemberResponse memberResponse : result) {
System.out.println("memberResponse = " + memberResponse);
}
}
Projections.constructor
값을 넘길 때 생성자와 순서가 맞아야 컴파일 에러가 안나며 , @AllArgsConstructor 가 필요하다.
@Test
@DisplayName("쿼리디에스엘 dto 생성자접근")
void dtoByConstructer() throws Exception {
//dto로 조회
List<MemberResponse> result = jpaQueryFactory.select(
Projections.constructor(MemberResponse.class,
member.username,
member.age)
).from(member)
.fetch();
for (MemberResponse memberResponse : result) {
System.out.println("memberResponse = " + memberResponse);
}
}
@Data
public class MemberResponse {
private String username;
private int age;
public MemberResponse() {
}
@QueryProjection // <- 생성자에 @QueryProjection를 붙여주자
public MemberResponse(String username, int age) {
this.username = username;
this.age = age;
}
}
@QueryProjection을 이용하면 불변 객체 선언, 생성자를 그대로 사용할 수 있기 때문에 권장되는 패턴이다. 정확하게는 DTO의 생성자를 사용하는 것이 아니라 DTO 기반으로 생성된 QDTO 객체의 생성자를 사용하는 것이다. 이제 다음에는 QType class를 생성해 볼것이다.
@QueryProjection 어노테이션을 달아주고, compileQuerydsl을 실행해주면 DTO도 Q파일로 생성을 해준다. Contructor는 컴파일 오류를 잡지 못하고 런타임 오류가 일어나기에 유저가 코드를 실행하는 순간 문제를 발견할 수 있지만, QueryProjection은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 하지만 DTO에 QueryDSL 어노테이션을 유지해야 하기에 QueryDSL에 종속적인 점과 DTO까지 Q파일을 생성해야 하는 단점이 있다.
@Test
@DisplayName("@QueryProjection")
void dtoByAnnotaion() throws Exception {
//컴파일시 에러를 잡아줄 수 있다.
List<MemberResponse> memberResponseList = jpaQueryFactory
.select(new QMemberResponse(member.username, member.age))
.from(member)
.fetch();
for (MemberResponse memberResponse : memberResponseList) {
System.out.println("memberResponse = " + memberResponse);
}
}
select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
출력🔽
memberResponse = MemberResponse(username=member1, age=10)
memberResponse = MemberResponse(username=member2, age=20)
memberResponse = MemberResponse(username=member3, age=30)
memberResponse = MemberResponse(username=member4, age=40)
와 감사해요 어려운 부분이였는데 많이 배워갑니다 ^^