JPA에서 페이징 처리를 공부하면서 적용해봤습니다. 배경을 설명해드리자면, 주문 조회 시 시작 날짜와 종료 날짜를 입력받아 페이징 처리하여 보여주는 기능을 구현했습니다.
package assignment.shop.order.repository;
import assignment.shop.order.Order;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.time.LocalDateTime;
import java.util.List;
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query(value = "select distinct o from Order o " +
" join fetch o.orderItems oi " +
" join fetch oi.item i " +
"where o.memberId = :memberId " +
"and o.createdDate between :startDate and :endDate")
Page<Order> findByUserIdAndDateCondition(Long memberId, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
}
우선 repository를 살펴보면 유저의ID와 시작 날짜 ~ 종료 날짜를 기준으로 쿼리를 가져오고 있습니다. 혹시 잘못된 부분이 보이시나요?
위 상태로 was를 올리게 되면 아래와같은 오류가 납니다.
Caused by: java.lang.IllegalArgumentException: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=oi,role=assignment.shop.order.Order.orderItems,tableName=order_item,tableAlias=orderitems1_,origin=orders order0_,columns={order0_.order_id,className=assignment.shop.order.OrderItem}}] [select count(distinct o) from assignment.shop.order.Order o join fetch o.orderItems oi join fetch oi.item i where o.memberId = :memberId and o.createdDate between :startDate and :endDate]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:138) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:757) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:114) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:362) ~[spring-orm-5.3.22.jar:5.3.22]
at jdk.proxy3/jdk.proxy3.$Proxy153.createQuery(Unknown Source) ~[na:na]
at org.springframework.data.jpa.repository.query.SimpleJpaQuery.validateQuery(SimpleJpaQuery.java:90) ~[spring-data-jpa-2.7.2.jar:2.7.2]
... 77 common frames omitted
이러한 오류가 나는 이유는 이 부분 과 같습니다.
우선 페이징을 하기 위해서는 전체 카운트가 꼭 있어야 합니다. 그래야 몇 page까지 있는지 알 수 있습니다.
그래서 countQuery가 없으면 스프링 데이터 JPA가 임의로 원본 쿼리를 보고 countQuery를 생성합니다.
그런데 이 임의로 생성하는 쿼리가 그렇게 똑똑하지는 못해서 다음과 같이 만들어냅니다.
원본쿼리: select u from User u join fetch u.store s
임의로 생성한 count쿼리: select count(u) from User u join fetch u.store s
그런데 여기서 문제는 fetch join은 객체 그래프를 조회하는 기능이기 때문에 연관된 부모가 꼭 있어야 합니다. 그런데 수를 뽑는 count(u)로 조회 결과가 변경되어버렸기 때문에, 오류가 발생한 것이지요.
그래서 fetch join이나 복잡한 쿼리의 경우 꼭 countQuery를 분리해서 사용해주세요^^
이러한 이유로 아래와 같이 countQuery를 분리해보았습니다.
package assignment.shop.order.repository;
import assignment.shop.order.Order;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.time.LocalDateTime;
import java.util.List;
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query(value = "select distinct o from Order o " +
" join fetch o.orderItems oi " +
" join fetch oi.item i " +
"where o.memberId = :memberId " +
"and o.createdDate between :startDate and :endDate", countQuery = "select count(o) from Order o")
Page<Order> findByUserIdAndDateCondition(Long memberId, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
}
service 쪽을 확인해보겠습니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ItemRepository itemRepository;
/**
* 유저의 주문내역 조회 페이징 처리
*/
public Page<OrderHistoryResponse> getOrderHistory(Long memberId, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {
Page<Order> orders = orderRepository.findByUserIdAndDateCondition(memberId, startDate, endDate, pageable);
return orders.map(OrderHistoryResponse::from);
}
}
유저의 ID, 시작 날짜, 종료 날짜와 페이징 사이즈를 받아서 findByUserIdAndDateCondition를 실행시킨 값을 service 단에서 response로 변환하여 반환해줍니다.
controller 단을 확인해보면
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private static final String USER_ID_HEADER = "x-user-id";
private final OrderService orderService;
/**
* 유저 주문내역 조회[페이징] API
* @param memberId
* @param startDate
* @param endDate
* @param pageable
* @return Page<OrderHistoryResponse>
*/
@GetMapping("/history")
public Page<OrderHistoryResponse> getOrderHistory(@RequestHeader(USER_ID_HEADER) Long memberId,
@RequestParam("start_date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam("end_date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@PageableDefault(size = 5) Pageable pageable) {
if (startDate.isAfter(endDate)) {
throw new OrderException(ErrorCode.INVALID_INPUT_VALUE);
}
return orderService.getOrderHistory(memberId, startDate, endDate, pageable);
}
}
시작날짜와 종료 날짜 페이지 사이즈를 받아와서 처리해줍니다. 결과를 확인해볼까요?
페이징을 처리한것을 볼 수 있습니다. 인수 테스트 중 하나 정도 확인해보겠습니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OrderAcceptanceTest {
@LocalServerPort
int port;
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
public void 주문_내역을_조회시_기간으로_조건에_맞는_주문이_있으면_주문_페이징_목록이_반환() {
given()
.accept(MediaType.APPLICATION_JSON_VALUE)
.header("x-user-id", "1")
.when()
.get("/api/orders/history?start_date=2022-06-20T00:00&end_date=2023-08-21T00:00")
.then()
.statusCode(HttpStatus.OK.value())
.body("first", equalTo(true))
.body("size", equalTo(5));
}
...
}
확인 결과 정상적으로 작동하는 것을 볼 수 있습니다. SpringDataJPA를 사용하면서 Page을 손쉽게 할 수 있도록 지원해줘서 학습하여 적용해봤습니다.
'혼자 공부하는 것들 > JPA' 카테고리의 다른 글
[지구최강 JPA 스터디] 8-9주차 JPA의 프록시 활용 (0) | 2022.10.08 |
---|---|
[Spring + JPA] 동시성 문제를 해결해보자! (0) | 2022.09.09 |
[지구최강 JPA 스터디] 6-7주차 연관관계 매핑 4가지와 단방향, 양방향! (0) | 2022.08.14 |
[지구최강 JPA 스터디] 4-5주차 객체 설계 할때는 단방향으로!? (0) | 2022.07.28 |
[지구최강 JPA 스터디] 2-3주차 몰아서 정리하자! (1) | 2022.07.13 |
댓글