QueryDSL에서 추가로 기능 구현하기
기존의 QueryDSL은 단순 조회를 위해 사용하였다.
하지만 SQL을 사용할 때도 그랬듯 한가지 기능만으로 사용하기에는 결국 또 복잡해진다.
예시로 내가 하고싶은것은
"가게 조회 시 리뷰리스트 조회와 해당 가게에 등록되어 있는 리뷰들의 평점 평균을 알고싶다"
라는 쿼리문을 요청하려면 어떻게 해야 할까?
이거를 QueryDSL 식으로 표현하면
Querydsl를 이용하는 경우 엔티티와 다른 반환 타입인 경우 Projections를 사용한다.
Projections을 이용해서 projection 하는 방법은 크게 3가지가 있다.
1. Projections.bean을 이용하는 방법
2. Projections.constructor를 이용하는 방법
3. @QueryProjection를 사용하는 방법
이 중
1번
Projections.bean 방식은 setter 기반으로 동작하게 된다.
그러기 때문에 MemberDtoBean객체의 setter 메서드를 열어야 합니다.
일반적으로 Response, Request 객체는 불변 객체를 지향하는 것이 바람직하다고 생각하기 때문에 권장하지 않는다.
2번
Projections.constructor 방식은 생성자 기반이다.
@AllArgsConstructor가 필요하고 setter가 필요 없지만
값을 넘길 때 생성자와 순서가 맞아야 데이터를 불러와 사용자가 실수하기 쉽다.
3번
@QueryProjection 방식은 해당 어노테이션을 달아주고, compileQuerydsl을 실행하면 DTO도 Q파일로 생성해준다.
가장 안전한 방법이지만 DTO에 QueryDSL 어노테이션을 유지해야하기 때문에 QueryDSL에 종속과 DTO까지 Q파일을 생성해줘야한다.
여기서 내가 사용한 방법은 2번이다.
가장 익숙하기도 하고, 생성자를 만들면서 무엇을 넣을지 다시 한번 상기되어서 좋다고 생각한다.
예시
List<StoreReviewResponseDto> reviews = queryFactory
.select(
Projections.constructor(
StoreReviewResponseDto.class,
store.ratingAvg,
review.id,
review.store.id,
review.rating,
review.reviewText,
review.userName,
review.createdAt
)
)
.from(store)
.join(review)
.on(store.id.eq(review.store.id))
.where(store.name.eq(name))
.fetch();
위의 코드를 보면 이런식으로 Dto를 반환한다면 해당 Dto class도 선언 해 주고
store에서 평균평점과, 나머지는 review에서 가져오는 값들로 채워져있다.
개인적으로 알아보기 쉽다고 생각한다.
위의 QueryDSL을 받아주는 DTO 또한 같은 순서로 구성해야한다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StoreReviewResponseDto {
private int ratingAvg;
private Long id;
private Long storeId;
private int rating;
private String reviewText;
private String userName;
private LocalDateTime createdAt;
}
또한, 여기서 가져온 리뷰들의 평점 평균을 구한다면
ouble average = reviews.stream()
.map(StoreReviewResponseDto::getRating)
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
위 코드를 통해 계산한 후에
QueryDSL의 set 메서드는 NumberPath 타입의 필드를 설정할 때 Integer 타입의 값을 필요로 합니다. 그래서 double 값을 바로 사용하면 타입 불일치로 인해 오류가 발생하게 됩니다. 이 경우, double 값을 int로 변환해서 전달해 주면 문제를 해결할 수 있습니다.
queryFactory.update(store)
.set(store.ratingAvg, (int)average)
.where(store.name.eq(name))
.execute();
이런식으로 업데이트 해주면 된다(단, 해당 메서드 실행 시 @Transaction 필요)