본문 바로가기

심화 캠프 정리

동적쿼리 OrderSpecifier

심화 프로젝트를 진행하면서 동적쿼리에 대한 정리와 동적쿼리 중 정렬(orderBy)에 사용되는 OrderSpecifier을 사용하여 공통 코드를 작성해보려고 한다.

 

동적쿼리란 사용자의 입력에 따라 조건이나 정렬 방식을 유현하게 변경하는 기능이다.

리뷰를 별점이 있다고 가정했을때 별점 많은순 별점 적은순 등 사용자가 원하는 입력에 따라 결과를 변경할 수 있게 한다.

 

그중 OrderSpecifier는 QueryDSL에서 쿼리 결과를 정렬하기 위한 클래스 이다.

아래의 코드는 Pageable을 사용한 간단하게 작성한 공통 처리 코드이다. 

Entity, Controller, Dto, Serive, Repository를 간단하게 구성후 테스트를 진행

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
@Table(
    name = "p_store"
)
public class Store extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "id")
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Column(name = "name", nullable = false)
    private String name;

    @OneToMany(mappedBy = "store",fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<StoreCategory> storeCategory = new ArrayList<>();

    @Column(name = "address", nullable = false)
    private String address;

    @Column(name = "call_number", nullable = false)
    private String callNumber;

    @Column(name = "store_grade", nullable = false)
    private double storeGrade;

    @Column(name = "store_grade_reviews", nullable = false)
    private int storeGradeReviews;

}
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Setter
    @Column(name = "deleted_at")
    private LocalDateTime deletedAt;

    @Setter
    @CreatedBy
    @Column(name = "created_by", updatable = false)
    private UUID createdBy;

    @LastModifiedBy
    @Column(name = "updated_by")
    private UUID updatedBy;

    @Setter
    @Column(name = "deleted_by")
    private UUID deletedBy;

}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
@Tag(name = "매장 관련 API")
public class StoreController {

    private final StoreService storeService;

    @GetMapping("/stores/test")
    public ResponseEntity<CommonResponse<Slice<TestResponse>>> testStoreGradeQuery(
       Pageable pageable
    ){
       return CommonResponse.success(SuccessCode.SUCCESS,
          storeService.testStoreGradeQuery(pageable));
    }
}
@Getter
@AllArgsConstructor
public class TestResponse {
    private final UUID storeId;
    private final String storeName;
    private final double storeGrade;
}
@Service
@RequiredArgsConstructor
public class StoreService {

    private final StoreJpaRepository storeJpaRepository;
    private final StoreQueryRepository storeQueryRepository;

    @Transactional(readOnly = true)
    public Slice<TestResponse> testStoreGradeQuery(Pageable pageable){
       return storeQueryRepository.testStoreQuery(pageable);
    }
}
@Repository
@RequiredArgsConstructor
public class StoreQueryRepository {

    private final JPAQueryFactory jpaQueryFactory;

    QStore qStore = QStore.store;

    public Slice<TestResponse> testStoreQuery(Pageable pageable){

       List<TestResponse> testResponseList = jpaQueryFactory.query()
          .select(
             Projections.constructor(
                TestResponse.class,
                qStore.id,
                qStore.name,
                qStore.storeGrade
             )
          ).from(qStore)
          .orderBy(getOrderSpecifierStore(pageable))
          .limit(pageable.getPageSize() + 1)
          .fetch();

       boolean hasNext = testResponseList.size() > pageable.getPageSize();
       if (hasNext) {
          testResponseList.remove(testResponseList.size() - 1);
       }

       return new SliceImpl<>(testResponseList, pageable, hasNext);
    }


    private OrderSpecifier<?>[] getOrderSpecifierStore(Pageable pageable) {
       return pageable
          .getSort()
          .stream()
          .map(order -> {
             PathBuilder pathBuilder = new PathBuilder<>(QStore.store.getType(), QStore.store.getMetadata());
             return new OrderSpecifier(
                order.isAscending() ? Order.ASC : Order.DESC,
                pathBuilder.get(order.getProperty()));
          }).toArray(OrderSpecifier[]::new);
    }

}

매장 평점이 1, 2, 3 인 매장이 있다고 가정하고 평점으로 정렬 테스트를 진행

오름차순 정렬

 

내림차순 정렬

아래의 코드로 Pageable에 Entity의 필드값에 대한 정렬옵션을 받아서 공통으로 처리하는 메서드를 사용할 수 있지만 

    private OrderSpecifier<?>[] getOrderSpecifierStore(Pageable pageable) {
       return pageable
          .getSort()
          .stream()
          .map(order -> {
             PathBuilder pathBuilder = new PathBuilder<>(QStore.store.getType(), QStore.store.getMetadata());
             return new OrderSpecifier(
                order.isAscending() ? Order.ASC : Order.DESC,
                pathBuilder.get(order.getProperty()));
          }).toArray(OrderSpecifier[]::new);
    }

커스텀으로 OrderSpecifier 메서드를 만들어서 해당 객체에대해서 정렬 옵션을 받을수 있다. 아래의 코드는 Enum을 사용한 간단하게 구현한 예시이다.

만약 추가하고 싶다면 옵션을 추가해서 추가로 작성해서 사용하면 된다!

public enum SortOption {
    CREATED_AT_ASC,
    CREATED_AT_DESC,
    UPDATED_AT_ASC,
    UPDATED_AT_DESC,
    STORE_STAR_RATING, // 스토어 별점순
}
private OrderSpecifier<?> getOrderSpecifier(SortOption sortOption) {
    return switch (sortOption) {
       case CREATED_AT_DESC -> qStore.createdAt.desc();
       case UPDATED_AT_ASC -> qStore.updatedAt.asc();
       case UPDATED_AT_DESC -> qStore.updatedAt.desc();
       case STORE_STAR_RATING -> qStore.storeGrade.desc();
       default -> qStore.createdAt.asc();
    };
}

'심화 캠프 정리' 카테고리의 다른 글

ModelAttribute  (0) 2024.11.20
PageableArgumentResolver  (0) 2024.11.20
동적쿼리 BooleanExpression  (0) 2024.11.20
PreAuthorize  (2) 2024.11.19
생성자 수정자 자동으로 생성하기  (0) 2024.11.19