김영한님 강의에서 공부한 컬렉션 조회 최적화.
컬렉션 조회는 Entity에서 일대다 관계(OneToMany)를 조회하는 것이다.
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" + //Collection fecth join
" join fetch oi.item i", Order.class)
.getResultList();
}
OrderItems를 fetch join으로 한번에 불러온다 -> SQL이 1번만 실행된다.
'distinct'를 사용한 이유는 일대다(OneToMany)의 경우 join을 하면 데이터의 row가 증가하기 때문이다.
JPA의 distinct는 SQL에도 distinct를 추가하고 Order 엔티티가 조회되면 애플리케이션에서 중복을 제거하고 orderItems는 Collection에 모아준다.
이 방법의 가장 큰 단점은 페이징이 불가능하다는 것이다.
Collection fetch join을 했을 때 페이징을 하면, 모든 데이터를 DB에서 읽은 후 메모리에서 페이징처리한다.
Collection fetch join은 1개만 사용해야 한다. 둘 이상의 fetch join을 사용하면 데이터가 부정합하게 조회 될 수 있다.
페이징 + Collection Entity를 함께 조회하는 방법은 아래 방법으로 대부분 가능하다.
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@Repository
public class OrderRepository {
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
}
*ToOne관계는 fetch join으로 최적화.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
장점
1. 쿼리 호출 수가 N+1 -> 1+1로 최적화 된다.
2. Join보다 DB데이터 전송량이 최적화 된다.
3. fetch join에 비해 쿼리 호출 수가 약간 증가하지만 전체적인 DB데이터 전송량이 감소한다.
4. 페이징이 가능하다!!
ToOne관게는 fetch join을 해도 페이징에 영향을 주지않는다. 따라서 ToOne관계는 fetch join으로 쿼리 수를 줄이고 나머지는 위의 옵션으로 해결하자.
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
//모든 데이터를 join해서 하나로 받은 후
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
//개발자가 직접 API 스펙에 맞는 DTO에 매핑함.
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(),
e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new" +
" jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDto.class)
.getResultList();
}
장점
1. 쿼리 한번에 모든 조회가 끝나버린다.
단점
1. 쿼리는 한번이지만 join으로 인해 더 느릴 수 있다.
2. 애플리케이션에서 데이터를 일일히 DTO에 매핑하는 추가 작업이 크다.
3. 페이징이 불가능하다.
그냥 참고
조회 시 양방향 연관관계에서 무한루프에 걸리지 않게 하려면 한곳에 '@JsonIgnore'를 추가해야한다.