상황 🥊
여기서 회원(나)의 모든 주문 정보를 가져오는 Query
를 작성하려면 회원의 Id를 기반으로 모든 정보를 가져올 수있어야 한다.
기존 작성 N+1 쿼리 🕹
public MyPurchaseItemResponse getMyItem() {
User user = userUtil.findCurrentUser();
List<Order> orders = orderRepository.findByUser(user);
List<MyPurchaseResponse> myPurchaseItems = getMyPurchaseResponses(orders);
return new MyPurchaseItemResponse(myPurchaseItems);
}
userUtil.findCurrentUser() : 해당 회원의 정보를 Json Web Token으로 인증하여 회원의 정보를 찾아온다.
orderRepository.findByUser(user) : user
와 order
의 관계는 1:N
양방향 관계이므로 order
에서 회원을 참조하여 해당 회원이 주문한 주문 목록을 가져온다.
geyMyPurChaseResponses : 위에서 가져온 주문 목록으로 해당 상품의 정보를 가져온다.
private List<MyPurchaseResponse> getMyPurchaseResponses(List<Order> orders) {
List<MyPurchaseResponse> myPurchaseItems = new ArrayList<>();
for (Order order : orders) {
for (OrderItem orderItem : order.getOrderItems()) {
MyPurchaseResponse items = new MyPurchaseResponse(order.getId(), orderItem.getItem().getId(), orderItem.getItem().getName(),
orderItem.getTotalPrice(), orderItem.getCount(), orderItem.getPaymentMethod());
myPurchaseItems.add(items);
}
}
return myPurchaseItems;
}
order
를 2중 for문으로 돌려서 Dto에 값을 넣어주게 된다. 해당 로직은 LAZY
로 동작한다.{
"myPurchaseResponses": [
{
"orderId": 3,
"itemId": 1,
"itemName": "계란",
"totalPrice": 20000,
"count": 2,
"paymentMethod": "MONEY"
},
{
"orderId": 3,
"itemId": 2,
"itemName": "식빵",
"totalPrice": 30000,
"count": 3,
"paymentMethod": "MONEY"
},
{
"orderId": 3,
"itemId": 3,
"itemName": "새우깡",
"totalPrice": 4000,
"count": 4,
"paymentMethod": "MONEY"
},
{
"orderId": 5,
"itemId": 1,
"itemName": "계란",
"totalPrice": 30000,
"count": 3,
"paymentMethod": "MONEY"
},
{
"orderId": 5,
"itemId": 2,
"itemName": "식빵",
"totalPrice": 10000,
"count": 1,
"paymentMethod": "MONEY"
},
{
"orderId": 6,
"itemId": 1,
"itemName": "계란",
"totalPrice": 20000,
"count": 2,
"paymentMethod": "MONEY"
},
{
"orderId": 6,
"itemId": 2,
"itemName": "식빵",
"totalPrice": 30000,
"count": 3,
"paymentMethod": "MONEY"
},
{
"orderId": 6,
"itemId": 3,
"itemName": "새우깡",
"totalPrice": 4000,
"count": 4,
"paymentMethod": "MONEY"
}
]
}
OrderItem
의 수가 증가하면 그만큼 쿼리가 더 나가게 된다. (N+1)해결 방법 🐳
Order
를 fetch join
으로 가져오는 방법 @Query("select o from Order o join fetch o.user u join fetch o.orderItems oi join fetch oi.item i where u.id = :userId")
이렇게 Fetch join으로 한번에 모든 정보를 가져올 수 있다. 하지만 OneToMany 관계 에서는 fetch join 으로 데이터를 가져올 경우 데이터가 Many의 수만큼 뻥튀기가 되어 distinct
를 넣어주어야한다.
OneToMany 컬렉션 페치 조인을 사용하면 페이징이 불가능하다. DB에 있는 Data를 기준으로 페이징을 하기 때문에 1:다 관계에서는 페이징을 하면안된다.
컬렉션 페치 조인은 1개만 사용해야 한다.
new
명령어를 사용해서 객체로 바로 받아오기@Query("select new com.backend.pointsystem.dto.response.MyPurchaseResponse(o.id, i.id, i.name, oi.totalPrice, oi.count, oi.paymentMethod) from Order o inner join o.user u inner join o.orderItems oi inner join oi.item i where u.id = :userId")
List<MyPurchaseResponse> findMyOrders(@Param("userId") Long userId);
위와 같이 new 명령어를 통해 Dto
로 필요한 정보만 가져올 수 있다. 테이블의 Join은 dto로 가져오는 것 자체가 fetch join 결과이므로 inner join을 사용하였다.
NEW 명령어를 사용하려면 아래 2가지를 주의해야 한다.
public MyPurchaseItemResponse getMyItem() {
User user = userUtil.findCurrentUser();
return new MyPurchaseItemResponse(orderRepository.findMyOrders(user.getId()));
}
위와 같이 그냥 쿼리 자체에서 모든 데이터를 가져와서 DTO로 반환해주기 때문에 전의 코드에 비해서 엄청나게 코드가 간소해졌다.
select
user0_.user_id as user_id1_5_,
user0_.created_at as created_2_5_,
user0_.updated_at as updated_3_5_,
user0_.asset as asset4_5_,
user0_.name as name5_5_,
user0_.password as password6_5_,
user0_.point as point7_5_,
user0_.username as username8_5_
from
user user0_
where
user0_.username=?
select
order0_.order_id as col_0_0_,
item3_.item_id as col_1_0_,
item3_.name as col_2_0_,
orderitems2_.total_price as col_3_0_,
orderitems2_.count as col_4_0_,
orderitems2_.payment_method as col_5_0_
from
orders order0_
inner join
user user1_
on order0_.user_id=user1_.user_id
inner join
order_item orderitems2_
on order0_.order_id=orderitems2_.order_id
inner join
item item3_
on orderitems2_.item_id=item3_.item_id
where
user1_.user_id=?
위와 같이 같은 데이터를 가져오는데 user[1], order[1] 총 2번의 select
쿼리가 나가게 된다.
결론 🥲
fetch join 이나 new 명령어로 N+1 문제를 해결하자.