티스토리 뷰

1 : N  관계에서 Fecth Join 사용시 발생하는 MultipleBagFetchException 해결법

1. ToOne(OneToOne, ManyToOne) 관계를 모두 Fetch Join 한다.

2. 1:N 관계의 Collection Entity는 Lazy Loading(지연 로딩)으로 조회한다.

3. Lazy Loading(지연 로딩) 성능 최적화를 위해 hibernate.default_batch_fetch_size를 적용한다.

 

* QueryDsl 에서도 1:N 관계에서 단 한번의 fetch join 사용해야 하는 건 똑같다.

 

 

 

ToOne(OneToOne, ManyToOne) 관계를 모두 Fetch Join 한다.

연관관계에서 자식 테이블인 ToOne 관계에서는 row 수를 증가시키지 않는다.

따라서 fetch join을 몇개를 사용하든 페이징 쿼리에 영향을 주지 않는다.

 

  • Order : 연관관계 주인
  • member(ManyToOne) / delivery(OneToOne) : 자식
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();
}

 

result query

Hibernate: 
    select
        order0_.order_id as order_id1_6_0_,
        member1_.member_id as member_i1_4_1_,
        delivery2_.delivery_id as delivery1_2_2_,
        order0_.delivery_id as delivery4_6_0_,
        order0_.member_id as member_i5_6_0_,
        order0_.order_date as order_da2_6_0_,
        order0_.status as status3_6_0_,
        member1_.city as city2_4_1_,
        member1_.street as street3_4_1_,
        member1_.zipcode as zipcode4_4_1_,
        member1_.name as name5_4_1_,
        delivery2_.city as city2_2_2_,
        delivery2_.street as street3_2_2_,
        delivery2_.zipcode as zipcode4_2_2_,
        delivery2_.status as status5_2_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 limit ? // 페이징 처리 문제없이 됨

 

 

 

1 : N 관계의 Collection / Proxy Entity는 Lazy Loading(지연 로딩)으로 조회한다.

지연 로딩(LAZY LOADING)

실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용하는 시점에 영속성 컨텍스트를 통해 entity에 필요한 데이터를 조회한다.

 

1. order 주문 조회

order : orderRepository.findAllWithMemberDelivery()

 

2. orderItems 주문 정보 조회

orderItems : order에 대한 각각의 주문정보 조회하기 위해 stream으로 반복

@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;
}

 

3. orderDto에서 order.getXX()~ : 지연 로딩으로 프록시 객체 조회

public OrderDto(Order order) {
    orderId = order.getId();
    name = order.getMember().getName();
    orderDate = order.getOrderDate();
    orderStatus = order.getStatus();
    address = order.getDelivery().getAddress();
    orderItems = order.getOrderItems().stream()
            .map(orderItem -> new OrderItemDto(orderItem))
            .collect(toList());
}

 

4. orderItemDto 분리

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

 

 

 

Lazy Loading(지연 로딩) 성능 최적화를 위해 hibernate.default_batch_fetch_size를 적용한다.

application.yml

jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        default_batch_fetch_size: 100

size : {개수}

  • 설정한 개수만큼 in query로 조회한다.
  • 개수는 100 ~ 1000 사이로 선택한다.
  • 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하는 경우도 있어서 max사이즈로는 1000 설정한다.

 

개수 정하는 기준은 몇개?

  • 100 설정 : 데이터가 1000개 있을 때, 10번의 쿼리가 나가게 된다. ⇒ 사이즈 적으면 날리는 쿼리 개수가 많아질 확률이 높아진다.
  • 1000 설정 : 한번에 1000개를 하나의 쿼리로 불러오기 때문에 DB 부하가 순간적으로 증가한다. ⇒ WAS, DB가 버틸 수 있는지 판단해서 결정해야한다.

 

메모리 입장에서 생각해보면?

  • WAS 입장에서는 100이든 1000개든 DB에서 최종적으로 가져와야하는 데이터 개수는 똑같다.
    • 대부분의 경우 반복으로 데이터 가져오면 결과값을 중간에 끊고 가져오지 않고,
      처음부터 끝까지 가져와서 WAS입장에서는 반복문 끝날 때 까지 기다리기 때문에 outofmemory 발생활 확률은 동일함.
    orderItems = order.getOrderItems().stream()
                .map(orderItem -> new OrderItemDto(orderItem))
                .collect(toList());
    

 

 

 

default_batch_fetch_size 로 기대되는 결과 : 부모 엔티티의 key값을 IN query 조건으로 사용

1. Collection orderItems 조회 결과

Hibernate: 
    select
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.count as count2_5_0_,
        orderitems0_.item_id as item_id4_5_0_,
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_price as order_pr3_5_0_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id in (
            ?, ?
        )

where orderitems0_.order_id in (4, 11);

orderitems의 부모 객체인 order의 id key 값을 in query로 조건 검색했다. 👏🏻👏🏻

 

select where order_id = ""

select where order_id = ""

order가 두개라 두번 나가던 쿼리를 in 조건으로 한번에 조회 성공!

 

List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);

orders 조회한 결과 개수 만큼, 미리 orderItem을 조회해온다.

 

 

2. item 조회 결과

Hibernate: 
    select
        item0_.item_id as item_id2_3_0_,
        item0_.name as name3_3_0_,
        item0_.price as price4_3_0_,
        item0_.stock_quantity as stock_qu5_3_0_,
        item0_.artist as artist6_3_0_,
        item0_.etc as etc7_3_0_,
        item0_.author as author8_3_0_,
        item0_.isbn as isbn9_3_0_,
        item0_.actor as actor10_3_0_,
        item0_.director as directo11_3_0_,
        item0_.dtype as dtype1_3_0_ 
    from
        item item0_ 
    where
        item0_.item_id in (
            ?, ?, ?, ?
        )

where item0_.item_id in (2, 3, 9, 10);

in query 로 한번에 조회 성공! 👏🏻👏🏻

 

 

결론

1 : N 관계가 여러개여서 fetch join을 사용할 수 없는 경우 default_batch_fetch_size을 사용해서 1 + N 쿼리를 → 1 + 1 으로 최적화 할 수 있다.

 

 

+ querydsl 에서 한 쿼리문에서 fetchJoin 여러개 사용하고 있는데 toOne 관계여서 가능한걸 확인했다.

나중에 1:N 연관관게에서 조인해야 하는 경우가 발생하면 추가로 작성하도록 하겠다.

 

 

 

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함