상황설정 : 특정
유저가 주문한주문상품의상품이름에 대해키워드 검색후Paging 처리하여 반환
。유저 1:주문 N관계
。주문 1:주문상품 N관계
。주문상품 N:상품 1관계
주문상품에 대한DTO POJO 객체를 생성
。해당주문상품 Entity 객체를 그대로 반환하는 경우 해당Entity 객체와연관관계에 존재하는Entity가 같이 반환되므로 필요한Field만축약하여 담는 용도의POJO객체를 생성
▶DTO 객체를 정의하는 방법은 총 3가지 존재 정의하는 방법은 총 3가지 존재.
POJO객체
。@QueryProjection을DTO의생성자메서드에 선언하여컴파일시QClass 클래스가 등록되도록 설정public interface OrderProductResponse { @Schema(name = "OrderProductResponse.Search") record Search( Long id, Long orderId, String productName, Long quantity, String receiverName, OrderStatus orderStatus, LocalDateTime deliveredAt ){ @QueryProjection public Search{} } }
{도메인명}RepositoryCustom 인터페이스+{도메인명}RepositoryImpl 구현체생성
。{도메인명}RepositoryImpl 구현체는@Repository선언 및QueryDSL에 대한Configuration을 수행하는클래스에서 정의한JPAQueryFactory객체를의존성주입public interface OrderProductRepositoryCustom { Page<OrderProductResponse.Search> getOrderProductsByKeyword(Long userId, String keyword, Pageable pageable); }@Repository @RequiredArgsConstructor public class OrderProductRepositoryImpl implements OrderProductRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; @Override public Page<OrderResponse.Search> getOrderProductsByKeyword(Long userId, String keyword, Pageable pageable) { // 로직 } }
- 기존
JpaRepository 인터페이스를확장하는Repository 인터페이스에서 해당{도메인명}RepositoryCustom 인터페이스를 확장@Repository public interface OrderProductRepository extends JpaRepository<OrderProductEntity, Long>, OrderProductRepositoryCustom {}
{도메인명}RepositoryImpl 구현체에서 내부로직 작성
。내부Query에 필요한QClass객체를의존성주입
。select()절에 데이터를 받을QClass DTO 객체를 설정
。각필드에 대한 조건을 정의한BooleanExpression 객체를 정의 후JPAQueryFactory객체.where(BooleanExpression객체)에 정의
▶keyword == null인 경우where(null)로where이 적용되지않도록 설정
。Pageable 객체에 포함된page 번호(getOffset()) 및page에 표현될 데이터수(getPageSize())를 통해 해당 범위의데이터만 가져오기
▶ 향후 return될Page객체에는 해당데이터+데이터 총 갯수+Pageable객체를 포함하여 생성 후 반환@Repository @RequiredArgsConstructor public class OrderProductRepositoryImpl implements OrderProductRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; private final QMemberEntity qMember = QMemberEntity.memberEntity; private final QOrderEntity qOrder = QOrderEntity.orderEntity; private final QOrderProductEntity qOrderProduct = QOrderProductEntity.orderProductEntity; private final QProductEntity qProduct = QProductEntity.productEntity; // 키워드가 null인지 판별 및 null이 아닌경우 해당 Product의 name에 keyword를 포함하는것을 찾음 public BooleanExpression containsProductName(String keyword){ return (Strings.isNotBlank(keyword))? qProduct.name.containsIgnoreCase(keyword) : null; } @Override public Page<OrderProductResponse.Search> getOrderProductsByKeyword(Long userId, String keyword, Pageable pageable) { // BooleanBuilder booleanBuilder = new BooleanBuilder() .and(containsProductName(keyword)) .and(qMember.id.eq(userId)); // List<OrderProductResponse.Search> searches = jpaQueryFactory .select(new QOrderProductResponse_Search( qOrderProduct.id, qOrder.id, qProduct.name, qOrderProduct.quantity, qOrder.receiver.name, qOrder.orderStatus, qOrder.deliveredAt )).from(qMember) .join(qOrder).on(qMember.id.eq(qOrder.member.id)) .join(qOrderProduct).on(qOrder.id.eq(qOrderProduct.order.id)) .join(qProduct).on(qOrderProduct.id.eq(qProduct.id)) .where(booleanBuilder) .orderBy(qOrderProduct.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); // int total = jpaQueryFactory .select(new QOrderProductResponse_Search( qOrderProduct.id, qOrder.id, qProduct.name, qOrderProduct.quantity, qOrder.receiver.name, qOrder.orderStatus, qOrder.deliveredAt )).from(qMember) .join(qOrder).on(qMember.id.eq(qOrder.member.id)) .join(qOrderProduct).on(qOrder.id.eq(qOrderProduct.order.id)) .join(qProduct).on(qOrderProduct.id.eq(qProduct.id)) .where(booleanBuilder).fetch().size(); // Page<OrderProductResponse.Search> page = new PageImpl<OrderProductResponse.Search>(searches, pageable, total); // return page; } }
Service Layer에서서비스코드작성@RequiredArgsConstructor @Service @Transactional // 트랜잭션의 원자성 보장 public class MemberServiceImpl implements MemberService { private final OrderProductRepository orderProductRepository; @Override @Transactional public Page<OrderProductResponse.Search> getOrderProducts( String keyword, Pageable pageable, Long memberId ){ return orderProductRepository.getOrderProductsByKeyword( memberId, keyword, pageable ); } }
@ModelAttribute에 의해바인딩될Paging관련DTO작성 및컨트롤러정의
。해당DTO 객체를컨트롤러의매개변수에 선언
▶/orders?page=숫자1&size=숫자2로Query String을 전달하는 경우 해당DTO객체의Field로바인딩public record Paging( @Min(value = 1, message = "Page 번호는 1 이하이어야합니다.") int page, @Range(min = 1, max = 20, message = "size는 1 이상 20 이하이어야합니다.") int size ) { public Pageable getPageable() { return PageRequest.of(page - 1, size); } }// /orders?keyword=문자열&page=숫자&size=숫자 전달되는 경우 @GetMapping("/orders") @ResponseStatus(HttpStatus.OK) public ApiResult<Page<OrderProductResponse.Search>> getOrders( @RequestParam String keyword, @ModelAttribute Paging paging, @AuthenticationPrincipal DefaultCurrentUser defaultCurrentUser ){ Pageable pageable = paging.toPageable(); Page<OrderProductResponse.Search> result = memberservice.getOrderProducts( keyword, pageable, defaultCurrentUser.getId() ); return ApiResult.ok(result); }
테스트코드작성@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional @AutoConfigureMockMvc class MemberControllerTest { @Autowired MemberRepository memberRepository; @Autowired OrderRepository orderRepository; @Autowired OrderProductRepository orderProductRepository; @Autowired ProductRepository productRepository; @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; MemberEntity testMember; OrderEntity testOrder; OrderProductEntity testOrderProduct; ProductEntity testProduct; @BeforeEach void setUp() throws Exception { testMember = memberRepository.save( MemberEntity.normalMember( "wjdtn", "1234", "쩡수", "wjdtn747@naver.com", "010-7615-6014", Gender.MALE, LocalDate.now() ) ); testProduct = productRepository.save( ProductEntity.of( "계란소시지상품", 10000L, 100L ) ); testOrder = orderRepository.save( OrderEntity.of( new Receiver( "이정수", "주소", "01076156131" ), testMember ) ); testOrderProduct = orderProductRepository.save( OrderProductEntity.of( 4L, testOrder, testProduct ) ); } @Test void 키워드_주문상품_검색_성공() throws Exception { DefaultCurrentUser defaultCurrentUser = new DefaultCurrentUser( testMember.getId(), testMember.getLoginId() ); mockMvc.perform( get("/members/orders") .with(user(defaultCurrentUser)) .param("keyword", "계란") .param("page", "1") .param("size", "10") ).andExpect(status().isOk()) .andExpect(jsonPath("$.data.content[0].id").value(testOrderProduct.getId())) .andExpect(jsonPath("$.data.content[0].orderId").value(testOrder.getId())); } }
Page<Entity Class>:org.springframework.data.domain.Page
。클라이언트에게페이징 요소를 모두 포함한 최종페이징 결과를 포함하여반환용도로 활용되는인터페이스
Page<Entity> page = new PageImpl<Entity>(List<Entity>객체, Pageable객체 , 총 데이터 수);
▶구현체를 통해페이지 범위에 해당하는 조회된List<Entity객체>,Pageable(페이지 사이즈,페이지 번호),총 데이터 수를 포함하여Page객체를 생성 후클라이언트에게 반환
。Repository 인터페이스에서Query method(Page<Entity> findAllByNameContainin(keyword , pageable))를 정의하여pageable을 전달하여Page<Entity>로return받을 수 있다.
PageImpl<Entity Class>:org.springframework.data.domain.PageImpl
。Page<Entity Class>의구현체
▶생성자로데이터,총 페이지 수,총 데이터 수를 전달받아서객체생성
Pageable 인터페이스:org.srpingframework.data.domain.Pageable
페이징시페이지번호와페이지 사이즈를 포함하는DTO용도로 사용
Pageable pageable = PageRequest.of(page-1 , size);
、클라이언트에게Controller에서페이지번호와페이지 사이즈를 입력받아구현체를 생성 및Service로 전달되는DTO로 활용되거나 조회 후Page<Entity>에 포함되어클라이언트에게 반환되는 용도
▶페이지는1번부터 시작하나SQL에서offset 0 * 데이터수부터 시작하므로페이지-1처리
PageRequest:org.srpingframework.data.domain.PageRequest
。Pageable 인터페이스 구현체
▶Pageable pageable = PageRequest.of(페이지번호, 페이지사이즈)로Pageable 인터페이스 구현체생성
。생성자는protected로 보호되어있으므로정적팩토리메서드 : of로 생성