주문내역에서 추가로 주문한 상품정보를 추가로 조회해보자.
Order을 기준으로 컬렉션인 OrderItem와 Item이 필요로합니다.
조회 성능최적화에서는 OneToOne ,ManyToOny관계만 있었다. 이번에는 컬렉션인일대다 관계(OneToMany)를 조회하고 최적화 하는 방법을 알아보겠습니다.
주문 조회 V1: 엔티티 직접 노출
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequiredArgsConstructor
@RestController
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName());
}
return all;
}
}
orderItem, Item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한다.
양방향 연관관계면 무한루프에 걸리지않게 한곳에 @JsonIgnore을 추가해야한다.
엔티티를 직접 노출하므로 좋은방법이아니다.
주문 조회 V2: 엔티티를 DTO로 변환
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@RestController
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order o) {
orderId = o.getId();
name = o.getMember().getName();
orderDate = o.getOrderDate();
orderStatus = o.getStatus();
address = o.getDelivery().getAddress();
orderItems = o.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(Collectors.toList());
}
}
@Data
static class OrderItemDto {
private String itemName; //상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
}
ValueObject인 Address는 그대로 조회해도 상관없음.
지연로딩이 너무많은 SQL실행
SQL실행횟수
order 1번 , member,address N번(order조회수),orderItem N번(order조회수),item N번(order조회수)
주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> allWithItem = orderRepository.findAllWithItem();
for (Order order : allWithItem) {
System.out.println("order ref="+order+" order.getId() = " + order.getId());
}
List<OrderDto> collect = allWithItem.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
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" +
" join fetch oi.item i", Order.class
).getResultList();
}
패치 조인으로 인해 SQL이 한번만 실행된다.
onyToMany관계일떄 distinct를 사용하지않으면 row 수가 One의 갯수가 아닌 Many의 갯수만큼 증가한다.
그 결과 distinct를 빼면 Order엔티티의 조회수도 증가하게된다.
문제점: 페이징불가능. out of memory 예외가 발생할 수 있습니다.
참고:컬렉션 페치조인은 1개만 사용할 수 있다. 컬렉션 둘 이상 페치조인을 사용하면 데이터의 정합성이 떨어질뿐만아니라
N*N개로 조회되는 중복된 ROW의 수가 기하급수적으로 늘어남.
주문 조회 V3.1: 엔티티를 DTO로 변환 - 페이징과 한계 돌파
페이징+컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까 ?
먼저 ToOne(OneToOne,ManyToOne)관계를 모두 페치조인 한다. ToOne관계는 row수를 증가시키지 않으므로
페이징 쿼리에 영향을 주지않는다.
컬렉션은 지연로딩으로 조회한다.
지연 로딩 성능 최적화를 위해 hibernate.defalut_batch_fetch_size,@BatchSize를 적용한다.
이 옵션을 사용하면 컬렉션이나 ,프록시 객체를 한꺼번에 설정한 size만큼 in쿼리로 조회한다.
@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> allWithItem = orderRepository.findAllWithMemberDelivery(offset,limit);
for (Order order : allWithItem) {
System.out.println("order ref=" + order + " order.getId() = " + order.getId());
}
List<OrderDto> collect = allWithItem.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
장점:
쿼리 호출수가 1+N -> 1+1로 최적화
DB전송량이 감소한다.페이징가능
결론: ToOne관계는 페치조인해도 페이징에 영향을 주지않는다. 따라서 ToOne관계는 페치조인으로 쿼리 수를 줄여서해결하고, 나머지는 hibernate.default_batch_fetch_size 로 최적화하자.
참고: hibernate.default_batch_fetch_size 100~1000사이를 선택하는 것을 권장한다.
'JPA' 카테고리의 다른 글
사용자 정의 리포지토리 구성 및 페이징처리 (0) | 2023.01.24 |
---|---|
API 개발 고급 - 컬렉션 조회 최적화 정리 (0) | 2023.01.18 |
API 개발 기본 (0) | 2023.01.17 |
Mysql 과 JpaData를 활용하여 만든 BoardService (0) | 2023.01.17 |
ModelMapper 사용 법 (0) | 2023.01.16 |