@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4() {
return orderQueryRepository.findOrderQueryDtos();
}
}
Get 방식으로 메소드를 생성한다.
DTO로 직접 조회를 할 것이기 때문에, OrderQueryRepository
를 만든 뒤 findOrderQueryDtos()
메소드를 구현한다.
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name,o.orderDate,o.status,d.address)"
+ " from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return 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 = :orderId", OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
}
아래는 OrderQueryDto
의 코드이다.
@Data
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime localDateTime;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime localDateTime, OrderStatus orderStatus,
Address address) {
this.orderId = orderId;
this.name = name;
this.localDateTime = localDateTime;
this.orderStatus = orderStatus;
this.address = address;
}
}
Dto를 살펴보면 orderItems 컬렉션은 생성자에서 제외를 한다.
OrderQueryRepository
의 findOrderQueryDtos()
를 살펴보면, 컬렉션을 별도의 Dto를 통해 가져온 뒤 OrderQueryDto
에 Setter로 추가해주는 방식을 택하고 있기 때문이다.
정리
- 즉, ToOne 관계는 join을 해도 데이터의 row 수가 증가 하지 않기 때문에
최적화하기 쉽기 때문에 한 번에 조회한다.
- ToMany 관계는 join시 데이터의 row 수가 증가하기 때문에 최적화가 어렵다.
- 따라서, ToOne관계를 먼저 조회하고 ToMany 관계는 별도로 처리한다.
아래는 OrderItemQueryDto
이다.
@Data
public class OrderItemQueryDto {
@JsonIgnore
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
우리가 찾을 정보는 Order1개당 2개의 OrderItem이 있고,
Order는 2개가 존재한다.
즉 Order 2개 - OrderItem 4개인 셈이다.
select
order0_.order_id as col_0_0_,
member1_.name as col_1_0_,
order0_.order_date as col_2_0_,
order0_.status as col_3_0_,
delivery2_.city as col_4_0_,
delivery2_.street as col_4_1_,
delivery2_.zipcode as col_4_2_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
inner join
delivery delivery2_
on order0_.delivery_id=delivery2_.delivery_id
--------------------------------------------------------------
select
orderitem0_.order_id as col_0_0_,
item1_.name as col_1_0_,
orderitem0_.order_price as col_2_0_,
orderitem0_.count as col_3_0_
from
order_item orderitem0_
inner join
item item1_
on orderitem0_.item_id=item1_.item_id
where
orderitem0_.order_id=?
--------------------------------------------------------------
select
orderitem0_.order_id as col_0_0_,
item1_.name as col_1_0_,
orderitem0_.order_price as col_2_0_,
orderitem0_.count as col_3_0_
from
order_item orderitem0_
inner join
item item1_
on orderitem0_.item_id=item1_.item_id
where
orderitem0_.order_id=?
위 쿼리를 보면 Order
를 조회할 때 1번, 각각의 OrderItem
을 조회할 때 2번
총 3번의 쿼리가 발생한다.
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos() {
// query 1번 -> 결과는 N개
List<OrderQueryDto> result = findOrders();
// query N번
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
}
findOrders()
메소드를 통해 쿼리를 1번 날렸을 때 N번의 쿼리가 추가로 발생해서 N+1
문제가 발생하기 때문에 최적화가 필요해 보인다.
참고 :