• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

일대다 조인 질문

24.01.23 17:41 작성 조회수 117

0

안녕하세요 강사님!

프로젝트 진행중에 이해가 되지 않은 점이 생겨 질문드립니다..!

@Test
    @DisplayName("유저가 반품, 취소한 상품들을 조회할 수 있다. ")
    public void findDistinctWithDetailsByMemberId() {
        // given
        Member member = Member.builder().build();

        Order order = Order.builder()
                .member(member)
                .orderNo("123")
                .build();

        Product product1 = Product.builder().build();
        Product product2 = Product.builder().build();
        Product product3 = Product.builder().build();

        OrderDetail orderDetail1 = OrderDetail.builder()
                .order(order)
                .product(product1)
                .statusCode(StatusCodeType.RETURN_COMPLETED.getCode())
                .build();
        OrderDetail orderDetail2 = OrderDetail.builder()
                .order(order)
                .product(product2)
                .statusCode(StatusCodeType.ORDER_CANCEL.getCode())
                .build();
        OrderDetail orderDetail3 = OrderDetail.builder()
                .order(order)
                .product(product3)
                .statusCode(StatusCodeType.DELIVERY_DELAY.getCode())
                .build();

        order.addOrderDetail(orderDetail1);
        order.addOrderDetail(orderDetail2);
        order.addOrderDetail(orderDetail3);

        productRepository.saveAll(List.of(product1, product2, product3));
        memberRepository.save(member);
        orderRepository.save(order);

        // when
        List<Order> orders = orderRepository.findDistinctWithDetailsByMemberId(member.getId());

        // then
        assertThat(orders).hasSize(1)
                .extracting("orderNo")
                .contains("123");

        List<OrderDetail> orderDetails = orders.get(0).getOrderDetails();
        assertThat(orderDetails).hasSize(2);

    }
@Override
    public List<Order> findDistinctWithDetailsByMemberId(Long memberId) {

        BooleanExpression statusCondition = orderDetail.statusCode.eq(ORDER_CANCEL.getCode());
        BooleanExpression orCondition = statusCondition.or(orderDetail.statusCode.eq(RETURN_COMPLETED.getCode()));

        return queryFactory
                .selectDistinct(order)
                .from(order)
                .join(order.orderDetails, orderDetail).fetchJoin()
                .where(
                        order.member.id.eq(memberId),
                        orCondition
                )
                .fetch();
    }

다음과 같이 테스트를 작성했습니다.

memberId와 ORDER_CANCEL,RETURN_COMPLETED 상태로 걸러서 최종적으로 원하는 orderDetail이 2개 나올 것으로 예상했습니다. 그러나 예상과 달리 3개의 orderDetail이 나왔습니다.

em.flush();
em.clear();

// when
        List<Order> orders = orderRepository.findDistinctWithDetailsByMemberId(member.getId());

when절 이전에 강제로 영속성 컨텍스트를 플러시하고 클리어 해주니 예상대로 orderDetail이 2개가 반환되었습니다.

 

영속성 컨텍스트에서 어떠한 문제가 있는 것 같은데 감도 안잡혀서 이렇게 질문 드립니다...ㅠㅠ 감사합니다.

 

아래는 JPA가 날린 쿼리입니다..!

select
        distinct o1_0.order_id,
        o1_0.address_id,
        o1_0.created_at,
        o1_0.deleted_at,
        o1_0.member_id,
        od1_0.order_id,
        od1_0.order_detail_id,
        od1_0.cancelled_at,
        od1_0.coupon_id,
        od1_0.created_at,
        od1_0.deleted_at,
        od1_0.delivered_date,
        od1_0.order_no,
        od1_0.payment_key,
        od1_0.price,
        od1_0.product_id,
        od1_0.quantity,
        od1_0.reason,
        od1_0.status_code,
        od1_0.updated_at,
        o1_0.order_name,
        o1_0.order_no,
        o1_0.real_price,
        o1_0.total_price,
        o1_0.total_used_coupon_price,
        o1_0.updated_at,
        o1_0.used_point 
    from
        orders o1_0 
    join
        order_detail od1_0 
            on o1_0.order_id=od1_0.order_id 
    where
        o1_0.member_id=? 
        and (
            od1_0.status_code=? 
            or od1_0.status_code=?
        )

 

답변 1

답변을 작성해보세요.

0

y2gcoder님의 프로필

y2gcoder

2024.01.24

안녕하세요. 유선목님, 공식 서포터즈 y2gcoder입니다.

일단 먼저 영속성 컨텍스트를 초기화해주지 않은 상태에서 해당 order 객체는 orderDetail 을 총 3개 들고 있는 것이 맞습니다. jpql로 필터링을 하더라도 기본적으로 JPA는 영속성 컨텍스트에 해당 @Id를 가진 객체가 존재하면 해당 id를 가진 객체를 그대로 사용합니다. 그래서 아마 영속성 컨텍스트 초기화 전에는 JPQL의 결과로 가져온 엔티티 결과를 버리고 영속성 컨텍스트에 있는 결과를 그대로 사용했기에 orderDetail이 3개가 존재한다는 결과를 뱉어낸 것으로 보입니다.

그리고 fetch join의 대상에 별칭을 줘서 필터링하는 방식을 사용하시면 예상치 못한 부작용이 발생할 수 있습니다. 해당 링크(클릭)을 참고해보시고 신중하게 사용하는 것을 추천합니다!

 

감사합니다.

유선목님의 프로필

유선목

질문자

2024.01.24

답변 감사합니다!

영속성 컨텍스트 초기화 전에 @id를 가지고 캐싱된 객체를 사용하는 것까지는 이해를 했습니다.

jpql로 필터링하더라도 이것을 버리고 캐싱된 orderDetail 3개를 모두 뱉어낸다는 말씀이 잘 와닿지 않는데 자세히 설명 부탁드려도 될까요??!

y2gcoder님의 프로필

y2gcoder

2024.01.24

기본적으로 동일한 영속성 컨텍스트 내에서 JPQL을 통해 이미 영속상태인 엔티티를 조회하면 DB에서 조회해온 것을 버리고 현재 영속성 컨텍스트 내에 있는 캐싱된 엔티티 객체를 사용한다는 뜻입니다! orderDetail 객체 3개와 연결된 order 객체가 영속성 컨텍스트에 존재하기 때문에, DB에서 필터링해온 orderDetail을 2개 가진 order 데이터는 버려지는 것입니다!

유선목님의 프로필

유선목

질문자

2024.01.24

이해했습니다! 감사합니다.
조회해온 것을 버린다고 하셨는데 영속성 컨텍스트에 있어도 jpql을 날리면 일단 db에 갔다온 후 영속성 컨텍스트에 존재한다면 db에서 가져온 데이터를 버리는 건가요?

영속성 컨텍스트에 @id 가 존재하면 굳이 db에 갔다오지 않아도 될 것 같아서 여쭤봅니다 ㅎㅎ

y2gcoder님의 프로필

y2gcoder

2024.01.24

넵 내부적으로 그렇게 구현되어있습니다! 보내드린 링크의 영한님 대답 중 2.영속성 컨텍스트와 2차 캐시 문제 의 흐름도 다시 한 번 읽어보시면 좋을 것 같습니다 :)

y2gcoder님의 프로필

y2gcoder

2024.01.24

추가로 다음 링크(클릭) 도 읽어보시길 추천합니다!

유선목님의 프로필

유선목

질문자

2024.01.24

친절한 답변 정말 감사드립니다!