스프링데이터 + JPA/API 개발

15. Dto 직접조회 컬렉션 최적화

sdafdq 2023. 11. 16. 10:53

지금은 컬렉션도 각각 Dto로 조회해오면서, batch가 안 먹히게 되었다. (정확히 등록된 엔티티를 조회해와야만 되는 듯 하다(fetch 한것 등))

 

그래서 지금 컬렉션은 ToOne이 아니라서 row 뻥튀기를 일으키게 하지 않기 위해 따로 넣어줬다.

 

그래서 이렇게 OrderItem을 넣어줄 때마다 또 그 OrderItem을 조회해주는 쿼리가 n번 나가게 되었다.

 

 

이제는 저 OrderItem 조회하는 걸 쿼리를 한번만 나가게끔 만들 것이다.

@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5(){
    return orderQueryRepository.findAllByDto_optimization();
}

 

public List<OrderQueryDto> findAllByDto_optimization() {
    List<OrderQueryDto> orders = findOrders();

    List<Long> orderIds = toOrderIds(orders);

    Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds);

    orders.forEach(o->o.setOrderItems(orderItemMap.get(o.getOrderId())));
    return orders;
}

orders는 먼저 만들어 놨던

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();
}

이걸 이용한다. Dto로 한번에 ToOne인 것들 같이 받아오는.

 

orderId들을 따로 뽑는 이유는, sql 쿼리 중 in을 이용하기 위해 그렇다.

private List<Long> toOrderIds(List<OrderQueryDto> orders) {
    return orders.stream()
            .map(o -> o.getOrderId())
            .collect(Collectors.toList());
}

그냥 map으로 order_id들만 뽑아서 List로 만드는 메소드

 

다음이 findOrderItemMap인데,

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();

    Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
            .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
    return orderItemMap;
}

먼저 Dto로 OrderItem들을 in으로 orderIds를 넣어서 조회해오고,

 

groupingBy는 말 그대로, 특정 값에 의해 묶을 수 있는 것인데, 

Map<묶을기준값, 묶인데이터> 형식으로 반환이 된다.

지금 같은 경우는 orderItemQueryDto들을 orderId로 묶었다.

 

List인 orderItems를 stream() 돌려서 각각의 원소를 그룹핑 해 주는데, orderItemQueryDto를 orderItemQueryDto의 orderId를 기준으로 묶는 것이다.

그렇게 각각의 orderItemQueryDto가, List<OrderItemQueryDto> 형태로 orderId 기준으로 묶인다.

 

그렇게 묶은 후, 그 Map<Long, List<OrderItemQueryDto>> 자체를 반환해 준다.

Long이 기준이 된 orderId다.

 

그러면 저기서 또 orders에다 forEach돌리는데,

각각의 order에다가 setOrderItems를 해 주는데, 그거를 반환받은 Map<Long, List<OrderItemQueryDto>>에서 order_id로 get해서 나온 List<OrderItemQueryDto>를 넣어 준다.

 

그냥 map형식의 콜렉션에서 get 해온거다.

 

여튼 그렇게 order가 완성이 된다.

 

 

즉, 먼저 ToOne인 것들은 다 가져오고,

order의 id들은 따로 뽑아서 List로 만든 다음 활용한다.

OrderItems들을 저 따로 뽑은 id들을 in의 파라미터로 넣어 조회해온다.

그 다음, 그렇게 뽑아놓은 OrderItems 뭉치들을 order_id 기준으로 나눠서 분리해 놓는다.

Map으로 하니 편하다.

Map 자체가 메모리에서 공간을 잡아두는 것이 아닌 형식을 만들어 두는 것이다.

 

그래서 처음 저런 식이면 

HashMap<T, K> hashMap = new HashMap<>();

for(OrderItem orderItem : orderItems){
    if(hashMap.get(orderItem.orderId) == null){
		haspMap.put(orderItem.orderId, new ArrayList<OrderItem>);
    }
    hashMap.get(orderItem.orderId).add(orderItem);
}

return hashMap;

groupingBy는 이런 식일 것이다.

 

여튼 저렇게 해서 받아 놓은 걸 order들에게 set 해놓고, 그 order들을 반환한다.

 

이러면 이제 OrderItem들을 조회할 때 쿼리를 in으로 묶어서 조회할 수 있다.

 

그러면 이제

    select
        o1_0.order_id,
        m1_0.name,
        o1_0.order_date,
        o1_0.status,
        d1_0.city,
        d1_0.street,
        d1_0.zipcode 
    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
2023-11-16T10:51:56.306+09:00  INFO 7652 --- [nio-8080-exec-2] p6spy                                    : #1700099516306 | took 0ms | statement | connection 22| url jdbc:h2:tcp://localhost/./jpashop
select o1_0.order_id,m1_0.name,o1_0.order_date,o1_0.status,d1_0.city,d1_0.street,d1_0.zipcode 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
select o1_0.order_id,m1_0.name,o1_0.order_date,o1_0.status,d1_0.city,d1_0.street,d1_0.zipcode 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;
2023-11-16T10:51:56.309+09:00 DEBUG 7652 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        o1_0.order_id,
        i1_0.name,
        o1_0.order_price,
        o1_0.count 
    from
        order_item o1_0 
    join
        item i1_0 
            on i1_0.item_id=o1_0.item_id 
    where
        o1_0.order_id in (?,?)

쿼리가 2개가 나간다.

처음 ToOne인거 모아서 new Operation Dto로 조회한 거 하나랑,

컬렉션들인 OrderItems를 따로 모아서 in으로 조회한 거 (아이템은 OrderItem 입장에서 ToOne이니 같이 조회함.)