Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NABI-258--FIX: cursor pagination #51

Merged
merged 8 commits into from
Nov 20, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.prgrms.nabimarketbe.domain.card.service.CardService;
import org.prgrms.nabimarketbe.domain.category.entity.CategoryEnum;
import org.prgrms.nabimarketbe.domain.item.entity.PriceRange;
import org.prgrms.nabimarketbe.global.util.OrderCondition;
import org.prgrms.nabimarketbe.global.util.ResponseFactory;
import org.prgrms.nabimarketbe.global.util.model.CommonResult;
import org.prgrms.nabimarketbe.global.util.model.SingleResult;
Expand Down Expand Up @@ -76,13 +77,17 @@ public ResponseEntity<SingleResult<CardPagingResponseDTO>> getCardsByCondition(
@RequestParam(required = false) String cursorId,
@RequestParam Integer size
) {
// TODO: 클라이언트에게 정렬 조건 받도록 추후에 수정하면 더 유연할 듯
OrderCondition condition = OrderCondition.CARD_CREATED_DESC;

CardPagingResponseDTO cardListReadPagingResponseDTO = cardService.getCardsByCondition(
category,
priceRange,
status,
cardTitle,
cursorId,
size
size,
condition
);

return ResponseEntity.ok(ResponseFactory.getSingleResult(cardListReadPagingResponseDTO));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.prgrms.nabimarketbe.domain.category.entity.CategoryEnum;
import org.prgrms.nabimarketbe.domain.item.entity.PriceRange;
import org.prgrms.nabimarketbe.domain.user.entity.User;
import org.springframework.data.domain.Pageable;

public interface CardRepositoryCustom {
CardPagingResponseDTO getCardsByCondition(
Expand All @@ -16,7 +17,7 @@ CardPagingResponseDTO getCardsByCondition(
List<CardStatus> status,
String title,
String cursorId,
Integer size
Pageable pageable
);

CardPagingResponseDTO getMyCardsByStatus(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package org.prgrms.nabimarketbe.domain.card.repository;

import com.querydsl.core.types.ConstantImpl;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.core.types.dsl.StringExpressions;
import com.querydsl.core.types.dsl.StringTemplate;
import com.querydsl.jpa.impl.JPAQueryFactory;
Expand All @@ -19,18 +18,19 @@
import org.prgrms.nabimarketbe.domain.item.entity.PriceRange;
import org.prgrms.nabimarketbe.domain.suggestion.dto.response.projection.SuggestionInfo;
import org.prgrms.nabimarketbe.domain.user.entity.User;
import org.prgrms.nabimarketbe.global.util.CursorPaging;
import org.prgrms.nabimarketbe.global.util.QueryDslUtil;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.util.ArrayList;
import java.util.List;

import static org.prgrms.nabimarketbe.domain.card.entity.QCard.card;
import static org.prgrms.nabimarketbe.domain.item.entity.QItem.item;
import static org.prgrms.nabimarketbe.domain.suggestion.entity.QSuggestion.suggestion;

@RequiredArgsConstructor
public class CardRepositoryImpl implements CardRepositoryCustom {
public class CardRepositoryImpl implements CardRepositoryCustom, CursorPaging {
private final JPAQueryFactory jpaQueryFactory;

@Override
Expand All @@ -40,7 +40,7 @@ public CardPagingResponseDTO getCardsByCondition(
List<CardStatus> status,
String cardTitle,
String cursorId,
Integer size
Pageable pageable
) {
List<CardListReadResponseDTO> cardList = jpaQueryFactory.select(
Projections.fields(
Expand All @@ -64,14 +64,11 @@ public CardPagingResponseDTO getCardsByCondition(
priceRangeEquals(priceRange),
titleEquals(cardTitle)
)
.orderBy(getOrderSpecifier(Sort.by(
Sort.Order.desc("createdDate"),
Sort.Order.desc("cardId")
)))
.limit(size)
.orderBy(QueryDslUtil.getOrderSpecifier(pageable.getSort(), card))
.limit(pageable.getPageSize())
.fetch();

String nextCursor = cardList.size() < size ? null : generateCursor(cardList.get(cardList.size() - 1));
String nextCursor = cardList.size() < pageable.getPageSize() ? null : generateCursor(cardList.get(cardList.size() - 1));

return new CardPagingResponseDTO(cardList, nextCursor);
}
Expand Down Expand Up @@ -103,7 +100,15 @@ public CardPagingResponseDTO getMyCardsByStatus(
card.status.eq(status),
card.user.eq(user)
)
.orderBy(card.createdDate.desc()) // 디폴트는 생성일자 최신순 정렬
.orderBy(
QueryDslUtil.getOrderSpecifier(
Sort.by(
Sort.Order.desc("createdDate"),
Sort.Order.desc("cardId")
),
card
)
)
.limit(size)
.fetch();

Expand Down Expand Up @@ -146,23 +151,27 @@ public List<CardSuggestionResponseDTO> getSuggestionAvailableCards(
return cardList;
}

private BooleanExpression cursorId(String cursorId) {
@Override
public BooleanExpression cursorId(String cursorId) {
if (cursorId == null) {
return null;
}

StringTemplate stringTemplate = Expressions.stringTemplate(
// 생성일자
StringTemplate dateCursorTemplate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
card.createdDate, // 디폴트는 생성일자 최신순 정렬
card.createdDate,
ConstantImpl.create("%Y%m%d%H%i%s")
);

return stringTemplate.concat(StringExpressions.lpad(
// pk
StringExpression pkCursorTemplate = StringExpressions.lpad(
card.cardId.stringValue(),
8,
'0'
))
.lt(cursorId);
);

return dateCursorTemplate.concat(pkCursorTemplate).lt(cursorId);
}

private BooleanExpression categoryEquals(CategoryEnum category) {
Expand Down Expand Up @@ -208,15 +217,4 @@ private String generateCursor(CardListReadResponseDTO cardListReadResponseDTO) {
.replace(":", "")
+ String.format("%08d", cardListReadResponseDTO.getCardId());
}

private OrderSpecifier[] getOrderSpecifier(Sort sort) {
List<OrderSpecifier> orders = new ArrayList<>();

for (Sort.Order order : sort) { // Sort에 여러 정렬 기준을 담을 수 있음
Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
orders.add(QueryDslUtil.getSortedColumn(direction, card, order.getProperty()));
}

return orders.toArray(OrderSpecifier[]::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import org.prgrms.nabimarketbe.domain.user.service.CheckService;
import org.prgrms.nabimarketbe.global.error.BaseException;
import org.prgrms.nabimarketbe.global.error.ErrorCode;
import org.prgrms.nabimarketbe.global.util.OrderCondition;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -202,20 +205,29 @@ public CardUserResponseDTO getCardById(

@Transactional(readOnly = true)
public CardPagingResponseDTO getCardsByCondition(
CategoryEnum category,
PriceRange priceRange,
List<CardStatus> status,
String title,
String cursorId,
Integer size
CategoryEnum category,
PriceRange priceRange,
List<CardStatus> status,
String title,
String cursorId,
Integer size,
OrderCondition orderCondition
) {

// 전달 받은 orderCondition 과 page size 에 맞게 pageRequest 를 구성 후 repo 에 넘김
PageRequest pageRequest = PageRequest.of(
0,
size,
getSortFromOrderCondition(orderCondition)
);

return cardRepository.getCardsByCondition(
category,
priceRange,
status,
title,
cursorId,
size
pageRequest
);
}

Expand Down Expand Up @@ -367,4 +379,16 @@ private List<CardImage> addThumbnail(

return newCardImages;
}

private Sort getSortFromOrderCondition(OrderCondition orderCondition) {
switch (orderCondition) {
case CARD_CREATED_DESC -> {
return Sort.by(
Sort.Order.desc("createdDate"),
Sort.Order.desc("cardId")
);
}
default -> throw new BaseException(ErrorCode.INVALID_ORDER_CONDITION);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.prgrms.nabimarketbe.global.util.QueryDslUtil;
import org.springframework.data.domain.Sort;

@Slf4j
@RequiredArgsConstructor
Expand Down Expand Up @@ -57,7 +59,15 @@ public HistoryListReadLimitResponseDTO getHistoryBySize(Integer size) {
.leftJoin(card).on(card.cardId.eq(completeRequest.fromCard.cardId))
.leftJoin(card).on(card.cardId.eq(completeRequest.toCard.cardId).as("toCardAlias"))
.where(completeRequest.completeRequestStatus.eq(CompleteRequestStatus.ACCEPTED))
.orderBy(completeRequest.modifiedDate.desc())
.orderBy(
QueryDslUtil.getOrderSpecifier(
Sort.by(
Sort.Order.desc("modifiedDate"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3
어느곳은 Sort 기준이 createdDate고, 어느곳은 modifiedDate인데요 ! 일관성을 가져갈 순 없는건가요 ??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 거래 성사 요청에 대한 부분이고, 거래 성사 같은 경우, 성사까지 이루어지는 시점으로 정렬 기준을 잡는 것이 좋아보여서 modifiedDate로 잡아두었다고 합니다.
From. 예진

Sort.Order.desc("completeRequestId")
),
completeRequest
)
)
.limit(size)
.fetch();

Expand Down Expand Up @@ -99,7 +109,15 @@ public HistoryListReadPagingResponseDTO getHistoryByUser(
.where(cursorIdLessThan(cursorId),
completeRequest.completeRequestStatus.eq(CompleteRequestStatus.ACCEPTED),
completeRequest.fromCard.user.eq(user).or(completeRequest.toCard.user.eq(user)))
.orderBy(completeRequest.modifiedDate.desc())
.orderBy(
QueryDslUtil.getOrderSpecifier(
Sort.by(
Sort.Order.desc("modifiedDate"),
Sort.Order.desc("completeRequestId")
),
completeRequest
)
)
.limit(size)
.fetch();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
import org.prgrms.nabimarketbe.domain.suggestion.entity.SuggestionType;
import org.prgrms.nabimarketbe.global.error.BaseException;
import org.prgrms.nabimarketbe.global.error.ErrorCode;
import org.prgrms.nabimarketbe.global.util.QueryDslUtil;
import org.springframework.data.domain.Sort;

import java.util.List;

import static org.prgrms.nabimarketbe.domain.card.entity.QCard.card;
import static org.prgrms.nabimarketbe.domain.completeRequest.entity.QCompleteRequest.completeRequest;
import static org.prgrms.nabimarketbe.domain.suggestion.entity.QSuggestion.suggestion;

@RequiredArgsConstructor
Expand Down Expand Up @@ -62,7 +65,15 @@ public SuggestionListReadPagingResponseDTO getSuggestionsByType(
.join(getQcardByDirectionType(directionType), card)
.on(getExpressionByDirectionType(directionType, cardId))
.where(cursorIdLessThan(cursorId), suggestionTypeEquals(suggestionType))
.orderBy(suggestion.createdDate.desc())
.orderBy(
QueryDslUtil.getOrderSpecifier(
Sort.by(
Sort.Order.desc("createdDate"),
Sort.Order.desc("suggestionId")
),
suggestion
)
)
.limit(size)
.fetch();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public enum ErrorCode {
CATEGORY_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "CA0001", "존재하지 않는 카테고리입니다."),
COMPLETE_REQUEST_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "CR0001", "존재하지 않는 제안 요청입니다."),
COMPLETE_REQUEST_MYSELF_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CR0002", "자신의 카드에는 거래 성사 요청을 할 수 없습니다."),
BATCH_INSERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "B0001", "이미지 저장 중에 문제가 발생했습니다.");
BATCH_INSERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "B0001", "이미지 저장 중에 문제가 발생했습니다."),
INVALID_ORDER_CONDITION(HttpStatus.INTERNAL_SERVER_ERROR, "O0001", "유효하지 않은 정렬 조건입니다.");

private HttpStatus status;
private String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.prgrms.nabimarketbe.global.util;

import com.querydsl.core.types.dsl.BooleanExpression;

public interface CursorPaging {
/**
* cursorId를 받아 cursor Id로 조건절을 만드는 메소드
* @param cursorId
* @return
*/
BooleanExpression cursorId(String cursorId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.prgrms.nabimarketbe.global.util;

public enum OrderCondition {
CARD_CREATED_DESC
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,35 @@
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.Expressions;
import org.springframework.data.domain.Sort;

import java.util.ArrayList;
import java.util.List;


public class QueryDslUtil {
/**
* 정렬을 기준을 반환하기 위한 메소드.
* 여러 정렬 조건들을 반환할 수 있다.
*
* @param sort Sort 객체
* @param parent compileQuerydsl 빌드를 통해서 생성된 Q타입 클래스의 객체(Sort의 대상이 되는 Q타입 클래스 객체를 전달한다.)
* @return
*/
public static OrderSpecifier[] getOrderSpecifier(
Sort sort,
Path<?> parent
) {
List<OrderSpecifier> orders = new ArrayList<>();

for (Sort.Order order : sort) { // Sort에 여러 정렬 기준이 담겨 올 수 있음
Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
orders.add(QueryDslUtil.getSortedColumn(direction, parent, order.getProperty()));
}

return orders.toArray(OrderSpecifier[]::new);
}

/**
* Order, Path, fieldName을 전달하면 OrderSpecifier 객체를 리턴하는 Util 클래스.
* Sort시 마다 사용할 수 있도록 한다.
Expand Down