그냥 join fetch 해서 가져올 거다.
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3(){
List<Order> orders = orderRepository.findAllWithItem();
return orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
}
public List<Order> findAllWithItem() {
return em.createQuery("select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item", Order.class)
.getResultList();
}
이러면 쿼리가 이제 하나로 나간다.
select
o1_0.order_id,
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status,
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name,
o1_0.order_date,
o2_0.order_id,
o2_0.order_item_id,
o2_0.count,
i1_0.item_id,
i1_0.dtype,
i1_0.name,
i1_0.price,
i1_0.stock_quantity,
i1_0.artist,
i1_0.etc,
i1_0.author,
i1_0.isbn,
i1_0.actor,
i1_0.director,
o2_0.order_price,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
join
delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
join
order_item o2_0
on o1_0.order_id=o2_0.order_id
join
item i1_0
on i1_0.item_id=o2_0.item_id
이렇게 하면, 결국 DB는 조회한 모든 데이터를 가져와야 하기 때문에,
이런 식으로 데이터가 뻥튀기 된다.
뭐 이게 잘못된 게 아니다. 당연히 올바른 동작이다.
여기서 distinct를 써도 그거는 데이터가 완전히 다 같아야 중복 제거가 되기 때문에 결과는 똑같다.
근데, 이 DB -> 객체로 오면서 생기는 문제가 있다.
저 DB가 객체로 오게 되면서 중복된 것도 그냥 List에 넣어진다. 즉, order들을 조회 해 왔는데 order_id 1인게 2개, 2인개 2개 이렇게 해서 총 4개가 된다.
객체는 그럴 필요가 없다.
참조로 관계가 정의되기 때문에 그냥 order 객체에 order_item이 2개 List로 넣어져 있으면 된다.
근데 result는 4개로 나와진다.
그래서, JPQL에서는 distinct를 쓰면 저런 주로 조회한 것의 id가 겹치는 것을 애플리케이션 상에서 따로 중복제거를 해준다.
그래서 써야 했었는데, 하이버네이트 6 오면서 패치가 되었다. distinct를 안써도 이제 그냥 알아서 애플리케이션 상에서 중복제거를 해준다.
그런데, DB상에서는 저렇게 쿼리가 나간다는 걸 알아야 한다.
저렇게 order_id 입장에서 중복된 결과가 나오는 건 정상적인 동작이다. DB는 조회한 결과를 모두 표시해야 하기 때문.
이걸 생각하고, 페이징을 한다고 생각해보자.
예를 들어,
public List<Order> findAllWithItem() {
return em.createQuery("select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item", Order.class)
.setFirstResult(0)
.setMaxResults(100)
.getResultList();
}
첫번째 데이터부터 100번째 데이터까지.
잘 감이 안오면, 뭐 0번부터 1번까지라고 생각해보자.
그럼 어떻게 나올 것 같나?
DB상에서는 이 상태에서 페이징 쿼리를 날리게 되면,
중간에 짤린다.
DB상에서는 총 4개의 데이터가 나왔다.
근데 여기서 sql에 페이징 쿼리를 날리게 되면 우리가 의도했던 데로 order_id가 1번인 것, order_id가 2번인 것이 나오는 게 아니라, order_id가 1번인 것만 2개 나오고 짤릴 것이다.
DB상에서는 그게 정상이다.
그래서, JPQL에서 저렇게 페이징을 쓰면,
2023-11-15T07:09:40.100+09:00 WARN 23912 --- [nio-8080-exec-1] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
firstResult/maxResults specified with collection fetch; applying in memory
firstResult, maxResult 그러니까 페이징 명령? 이 collection fetch와 함께 사용되었다고,
메모리 상에서 진행한다고 경고가 뜬다.
즉, 이렇게 collection에 fetch를 사용한 상태에서 페이징을 하면, 따로 sql에 페이징 쿼리가 나가는 게 아니라, 일단은 다 가져온 다음에, 메모리에서 처리 한다.
즉, 일단 다 가져온 다음에, 애플리케이션에서 가져온 Order를 기준으로 중복제거를 하고, 0번, 1번 그러니까 List<Order>에서 0번째 인덱스부터 1번째 인덱스까지 가져오는 것이다.
이게 생각보다 굉장히 치명적일 수 있는게, 데이터가 엄청나게 많은 경우 out of memory로 이어질 수 있다.
DB의 동작 따로, 애플리케이션의 동작 따로 생각해야 한다.
'스프링데이터 + JPA > API 개발' 카테고리의 다른 글
14. 컬렉션 Dto로 직접 조회 (0) | 2023.11.16 |
---|---|
13. 컬렉션 페이징 한계돌파 (0) | 2023.11.15 |
11. 컬렉션 엔티티를 Dto로 (0) | 2023.11.14 |
10. 컬렉션 조회 최적화 (0) | 2023.11.14 |
9. 바로 Dto로 받기 (0) | 2023.11.14 |