컬렉션 (= 일대다 관계) 조회, 최적화 해보자.
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAll();
// 직접 초기화 시키기
for (Order order : all) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); // 오더아이템도 직접 초기화
}
return all;
}
엔티티를 직접 노출하는 것은 좋지 않음 !
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAll();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
OrderApiController
@Data
static class OrderDto {
private Long orderId;
...
// 생성자
public OrderDto(Order order) {
// 초기화
orderId = order.getId();
name = order.getMember().getName();
...
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
...
}
지연 로딩으로 너무 많은 SQL이 실행됨.
SQL 실행 수 : 1+N
@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;
}
OrderRepository
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();
}
📍 distinct를 쓰는 이유?
-> Order을 기준으로 제시하면서 중복을 제거해주기 위해 distinct 추가해줌
💡 SQL 1번만 실행됨
- sql과 다르게 JPA에서는 id값이 같으면 중복처리 해주기 때문에 페치 조인으로 인해 쿼리가 1번 조회됨 !
💡 페이징 불가능
- 1:N 연관관계인 두 엔티티가 즉시 로딩된다면 (엔티티 그래프나 fetch join 으로 연결되어 있다면) 페이징 처리시 쿼리에 limit 가 붙지 않고 모든 데이터를 메모리에 불러오기 때문 !
▪️ 페이징?
예:
원하는 페이지 값과 사이즈를 넣어 데이터를 조회하는 것.
예를 들어 page=0, size=10 이면 첫번째 페이지의 주문 10개 결과가 나오게 된다.
페이징과 컬렉션 엔티티를 함께 조회하는 방법은?
applications.yml
에 설정 추가 or @BatchSize 사용spring: jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
-> 주문 id 를 마지막쿼리의 in 절에 넣어 해당 주문상품만 가져오게 되는 구조 !
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();
}
OrderApiController
@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;
}
1️⃣ 쿼리 호출 수가 1+N -> 1+1로 최적화됨.
2️⃣ 조인보다 DB 데이터 전송량이 최적화 됨.
(페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.)
❗️ why ? IN절에 객체 담아서 한번에 보내기 때문!
3️⃣ 페이징 가능
엔티티는 리포지토리, 화면이나 API에 의존관계가 있는 쿼리는 따로 클래스를 분리 !
- 루트 1번 + 컬렉션 N 번 쿼리 실행
(컬렉션을 루프로 직접 넣어주기 때문에 N번 걸림.)- ToOne(N:1, 1:1) 관계들을 먼저 조회하고, ToMany(1:N) 관계는 각각 별도로 처리
( row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화 하기 쉬우므로 한번에 조회하고, ToMany 관계는 최적화 하기 어려우므로 findOrderItems() 같은 별도의 메서드로 조회 )
OrderQueryRepository
public List<OrderQueryDto> findAllByDto_optimization() { //루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//orderItem 컬렉션을 MAP 한방에 조회
Map<Long, List<OrderItemQueryDto>> orderItemMap =
findOrderItemMap(toOrderIds(result));
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행X) in절에 담아서 한번에 가져오기
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long>
orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new
jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name,
oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
return orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}
1️⃣ Query: 루트 1번, 컬렉션 1번 (총 2번~)
2️⃣ ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem 을 한꺼번에 조회
3️⃣ MAP을 사용해서 매칭 성능 향상(O(1))
4️⃣ 데이터를 한꺼번에 처리할 때 많이 씀
OrderQueryRepository
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();
OrderFlatDto
package jpabook.jpashop.repository.order.query;
import jpabook.jpashop.domain.Address;
}
1️⃣ Query: 1번
2️⃣ 쿼리는 한번이지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되기 때문에 상황에 따라 V5 보다 더 느릴 수도 있음
3️⃣ 애플리케이션에서 추가 작업이 큼
4️⃣ 페이징 불가능
결론 !