주문 탐색(페이지) API는 고객에게 주문에 담긴 주문 상세, 상품 정보와 이미지 그리고 환불 정보까지 보여줄 필요가 있기 때문에 현재 로그인한 유저의 Profile_id를 이용해 Orders, OrderDetail, Product, Product_Image, Refund 정보를 가져와야한다.
그런데 Orders와 OrderDetail은 one-to-many 이기 때문에 fetch join을 통해서 가져올 경우 리턴되는 table의 record가 Orders의 갯수만큼 존재하는게 아니라 OrderDetail의 갯수만큼 존재하기 때문에 limit와 offset이 적용되지 않고 모든 데이터를 메모리로 가져온 후에 처리한다는 것을 이전 포스팅에서 언급한적이 있다.
따라서 이번에도 그 부분을 중점적으로 개선해보려고한다. 하는 김에 로직도 조금 바꾸고...
// OrdersRepositoryCustomImpl.java
@Override
public Page<Orders> findAllCreatedAfterAndProfile_Id(LocalDateTime createdAfterDateTime, Long profileId, Pageable pageable) {
QOrders order = QOrders.orders;
QOrderDetail orderDetail = QOrderDetail.orderDetail;
QPayment payment = QPayment.payment;
QProfile profile = QProfile.profile;
QRefund refund = QRefund.refund;
JPQLQuery<Orders> query =
from(order)
.innerJoin(order.profile, profile).fetchJoin()
.where(order.createdAt.gt(createdAfterDateTime)
.and(profile.id.eq(profileId))
.and(order.id.in(JPAExpressions.select(payment.order.id).from(payment).where(payment.paySuccessTf.eq(true)))))
.innerJoin(order.orderDetails, orderDetail).fetchJoin()
.where(orderDetail.id.notIn(JPAExpressions.select(refund.orderDetail.id).from(refund)))
.where(order.orderDetails.isNotEmpty());
List<Orders> orders = getQuerydsl().applyPagination(pageable, query).fetch();
return new PageImpl<>(orders, pageable, query.fetchCount());
}
위에 Query문을 간결하게 해설하자면 아래와 같다
"결제 성공한 Orders 중에 환불하지 않은 OrderDetail만을 join 해서, OrderDetail을 모두 환불한 경우는 제외하고 가져온다."
2023-03-29T04:13:12.311+09:00 WARN 10424 --- [nio-8080-exec-3] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
Hibernate:
select
o1_0.id,
o1_0.address,
o1_0.created_at,
o1_0.created_by,
o1_0.modified_at,
o1_0.modified_by,
o2_0.orders_id,
o2_0.id,
o2_0.created_at,
o2_0.created_by,
o2_0.modified_at,
o2_0.modified_by,
o2_0.product_id,
o2_0.product_review_id,
o2_0.quantity,
o2_0.status_type,
o1_0.payment_id,
p1_0.id,
p1_0.birth_date,
p1_0.email,
p1_0.gender,
p1_0.name,
p1_0.phone_number,
p1_0.point,
o1_0.receiver_name,
o1_0.receiver_phone_number
from
orders o1_0
join
profile p1_0
on p1_0.id=o1_0.profile_id
join
order_detail o2_0
on o1_0.id=o2_0.orders_id
where
o1_0.created_at>?
and o1_0.profile_id=?
and o1_0.id in(select
p2_0.order_id
from
payment p2_0
where
p2_0.pay_success_tf=?)
and o2_0.id not in(select
r1_0.order_detail_id
from
refund r1_0)
and exists(select
1
from
order_detail o5_0
where
o1_0.id=o5_0.orders_id)
order by
o1_0.created_at desc
Hibernate:
select
count(o1_0.id)
from
orders o1_0
join
order_detail o2_0
on o1_0.id=o2_0.orders_id
where
o1_0.created_at>?
and o1_0.profile_id=?
and o1_0.id in(select
p2_0.order_id
from
payment p2_0
where
p2_0.pay_success_tf=?)
and o2_0.id not in(select
r1_0.order_detail_id
from
refund r1_0)
and exists(select
1
from
order_detail o5_0
where
o1_0.id=o5_0.orders_id)
덕지덕지 붙어 있는 것이 누더기 골렘같다.. 실무에서도 이런 Query가 날아가는 일이 있을까?
있을 수도 있지만 내가 일하게 될 곳에서는 없었으면 좋겠다.
이번에도 batch_fetch
를 통해 one-to-many의 many 쪽의 Lazy Loading
을 모아서 처리할 생각이다. 다만 이번에는 이렇게 할 경우 발생하는 Query문이 최종적으로 4개가 된다.
(Orders select 1개
+ Orders count 1개
+ OrderDetail join Product, Refund select 1개
+ ProductImage select 1개
)
여기서 OrderDetail
과 Product
, Refund
는 one-to-one 관계들이기 때문에 join해서 1번에 가져오고 Product
와 ProductImage
는 one-to-many 이기 때문에 join 하지 않는다.
// OrdersRepositoryCustomImpl.java
@Override
public Page<Orders> findAllCreatedAfterAndProfile_Id(LocalDateTime createdAfterDateTime, Long profileId, Pageable pageable) {
QOrders order = QOrders.orders;
JPQLQuery<Orders> query =
from(order)
.where(order.createdAt.gt(createdAfterDateTime)
.and(order.profile.id.eq(profileId))
.and(order.payment.paySuccessTf.eq(true)));
List<Orders> orders = getQuerydsl().applyPagination(pageable, query).fetch();
return new PageImpl<>(orders, pageable, query.fetchCount());
}
필요없다고 생각되는 join과 로직을 제거했다. 바뀐 Query문에서는 로그인한 유저의 id와 검색 기간, 결제 성공 여부만 체크한다.
// OrderDetail.java
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class OrderDetail extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Orders orders;
@ManyToOne(optional = false)
@JoinColumn(name = "productId")
private Product product;
@Setter
@OneToOne(mappedBy = "orderDetail")
private Refund refund;
...
}
OrderDetail
을 조회할 때는 관련 정보인Product
와 Refund
를 반드시 조회 할 것이다. 때문에 이 2개에 연관관계에 대해서는 OrderDetail
을 조회할 때 위 코드와 같이 join 해서 조회하게 끔한다. (@JoinColumn
과 @OneToOne의 FK가 없는 쪽
을 활용)
Hibernate:
select
o1_0.id,
o1_0.address,
o1_0.created_at,
o1_0.created_by,
o1_0.modified_at,
o1_0.modified_by,
o1_0.payment_id,
o1_0.profile_id,
o1_0.receiver_name,
o1_0.receiver_phone_number
from
orders o1_0
join
payment p2_0
on p2_0.id=o1_0.payment_id
where
o1_0.created_at>?
and o1_0.profile_id=?
and p2_0.pay_success_tf=?
order by
o1_0.created_at desc offset ? rows fetch first ? rows only
Hibernate:
select
count(o1_0.id)
from
orders o1_0
join
payment p2_0
on p2_0.id=o1_0.payment_id
where
o1_0.created_at>?
and o1_0.profile_id=?
and p2_0.pay_success_tf=?
Hibernate:
select
o1_0.orders_id,
o1_0.id,
o1_0.created_at,
o1_0.created_by,
o1_0.modified_at,
o1_0.modified_by,
p1_0.id,
p1_0.allergy_info,
p1_0.brand,
p1_0.category_detail_code,
p1_0.content,
p1_0.country_of_origin,
p1_0.created_at,
p1_0.created_by,
p1_0.modified_at,
p1_0.modified_by,
p1_0.name,
p1_0.packaging,
p1_0.price,
p1_0.seller,
p1_0.shipping,
p1_0.unit,
p1_0.weight,
o1_0.product_review_id,
o1_0.quantity,
r1_0.id,
r1_0.cancel_reason,
r1_0.created_at,
r1_0.created_by,
r1_0.modified_at,
r1_0.modified_by,
r1_0.payment_key,
o1_0.status_type
from
order_detail o1_0
left join
product p1_0
on p1_0.id=o1_0.product_id
left join
refund r1_0
on o1_0.id=r1_0.order_detail_id
where
o1_0.orders_id in(?,?)
Hibernate:
select
p1_0.product_id,
p1_0.id,
p1_0.img_url
from
product_image p1_0
where
p1_0.product_id in(?,?,?)
앞에서 설명했듯 Orders select 1개
+ Orders count 1개
+ OrderDetail join Product, Refund select 1개
+ ProductImage select 1개
로 총 4개의 Qeury가 발생하고 이는 조회할 Orders
가 몇개든 동일하다.
사실 처음에는 Query문에서 환불 여부까지 체크해서 환불 안된 주문만 받아오려고 굉장히 오래 고민했다. 하지만 방법을 찾지못하고 고민하다가 반환하는 클라이언트에서 Refund
에 대한 정보를 포함하면 된다고 결론을 냈다.
일반적인 사이트에서 주문한 내역을 환불했다고 무조건 삭제하는 것이 정의는 아니니까, 또 그래야한다고 하더라도 클라이언트에서 Refund
존재 여부에 따라 보여줄지 말지를 결정하면된다.
그리고 이번 리팩토링에서 추가적으로 날아가는 select 문
에 join을 적용하는 법(@JoinColumn
이나 @OneToOne FK가 없는 쪽
등등)을 알아내 적용한 부분이 만족스러웠다.