Spring

QueryDSL 동적 정렬 쿼리 OrderSpecifier 구현하기

heyh0 2024. 3. 28. 16:06

#1. Event

구현해야 할 기능 화면
Repository 구조

#2. Feature

@Repository
public interface ReviewRepository extends JpaRepository<Review, Long>, ReviewRepositoryCustom {

	// 내용 생략 ...

}

@RequiredArgsConstructor
public class ReviewRepositoryImpl implements ReviewRepositoryCustom {

    private final JPAQueryFactory queryFactory;
    
    // 내용 생략 ...
    
}

 

상세한 구현을 위해 Custom, Impl 클래스를 생성했고 필요한 상속 관계를 추가했다.

Impl은 QueryDSL로 구현하기 때문에 JPAQueryFactory가 필요하다.

 

public enum RatingSortType {

    DATE,
    RATING_ASC,
    RATING_DESC,
    RATING_AVG_ASC,
    RATING_AVG_DESC;

}

 

정렬 타입을 enum으로 다음과 같이 분리했다.

 

import com.querydsl.core.annotations.QueryProjection;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class RatingFindAllResponse {

    private String isbn;

    private String title;

    private String image;

    private Double rating;

    @Builder
    @QueryProjection
    public RatingFindAllResponse(String isbn, String title, String image, Double rating) {
        this.isbn = isbn;
        this.title = title;
        this.image = image;
        this.rating = rating;
    }

}

 

응답해야 할 DTO는 다음과 같다.

현재 화면에서 필요한 isbn, title, image, rating 값만 필요하다.

이후에 더 필요한 내용은 isbn을 pk로 조회 쿼리를 날려서 찾을 수 있다.

 

import com.jisungin.application.review.response.RatingFindAllResponse;
import com.jisungin.domain.review.RatingSortType;

import java.util.List;

public interface ReviewRepositoryCustom {

    List<RatingFindAllResponse> findAllRatingOrderBy(Long userId, RatingSortType ratingSortType);

}
import com.jisungin.application.review.response.QRatingFindAllResponse;
import com.jisungin.application.review.response.RatingFindAllResponse;
import com.jisungin.domain.review.RatingSortType;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

import static com.jisungin.domain.book.QBook.book;
import static com.jisungin.domain.review.QReview.review;
import static com.jisungin.domain.review.RatingSortType.*;

@Slf4j
@RequiredArgsConstructor
public class ReviewRepositoryImpl implements ReviewRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public List<RatingFindAllResponse> findAllRatingOrderBy(Long userId, RatingSortType ratingSortType) {
        log.info("-------------------------------------------");
        return queryFactory.select(new QRatingFindAllResponse(
                        book.isbn, book.title, book.imageUrl, review.rating))
                .from(review)
                .leftJoin(book).on(review.book.eq(book))
                .where(review.user.id.eq(userId))
                .orderBy(createSpecifier(ratingSortType))
                .fetch();
    }

    private OrderSpecifier createSpecifier(RatingSortType ratingSortType) {
        if (ratingSortType.equals(RATING_ASC)) {
            return review.rating.asc();
        }
        if (ratingSortType.equals(RATING_DESC)) {
            return review.rating.desc();
        }
        if (ratingSortType.equals(RATING_AVG_ASC)) {
            return review.rating.avg().asc();
        }
        if (ratingSortType.equals(RATING_AVG_DESC)) {
            return review.rating.avg().desc();
        }
        return review.createDateTime.desc();
    }
    
}

 

리뷰와 책을 조인하고 가져온 데이터를 입력된 열거 타입으로 정렬했다.

 

OrderSpecifier 함수

해당 함수는 new OrderSpecifier(Order.ASC, review.rating)와 같은 인자 값을 넘기면 된다.

 

#3. Test

class ReviewRepositoryTest extends RepositoryTestSupport {

    @Autowired
    private ReviewRepository reviewRepository;

    @Autowired
    private BookRepository bookRepository;

    @Autowired
    private UserRepository userRepository;

    @DisplayName("리뷰 별점이 낮은 순으로 책을 정렬한다.")
    @Test
    void getRatingsOrderByRatingASC() {
        //given
        Book book1 = createBook("1");
        Book book2 = createBook("2");
        Book book3 = createBook("3");
        bookRepository.saveAll(List.of(book1, book2, book3));

        User user = createUser("1");
        userRepository.save(user);

        Review review1 = createReview(user, book1, 4.0);
        Review review2 = createReview(user, book2, 3.0);
        Review review3 = createReview(user, book3, 2.0);
        reviewRepository.saveAll(List.of(review1, review2, review3));

        //when
        List<RatingFindAllResponse> result = reviewRepository.findAllRatingOrderBy(
                user.getId(), RatingSortType.RATING_ASC);

        //then
        Assertions.assertThat(result)
                .extracting("isbn", "title", "image", "rating")
                .containsExactly(
                        tuple("3", "제목", "image", 2.0),
                        tuple("2", "제목", "image", 3.0),
                        tuple("1", "제목", "image", 4.0)
                );
    }

    private static Review createReview(User user, Book book, Double rating) {
        return Review.builder()
                .user(user)
                .book(book)
                .content("내용")
                .rating(rating)
                .build();
    }

    private static User createUser(String oauthId) {
        return User.builder()
                .name("김도형")
                .profileImage("image")
                .oauthId(
                        OauthId.builder()
                                .oauthId(oauthId)
                                .oauthType(OauthType.KAKAO)
                                .build()
                )
                .build();
    }

    private static Book createBook(String isbn) {
        return Book.builder()
                .title("제목")
                .content("내용")
                .authors("김도형")
                .isbn(isbn)
                .publisher("지성인")
                .dateTime(LocalDateTime.of(2024, 1, 1, 0, 0))
                .imageUrl("image")
                .build();
    }

}

 

테스트는 3개의 리뷰 데이터를 생성하고, 정렬 타입을 실행해서 결과가 올바른지 확인다.