티스토리 뷰
먼저 알아야 할 것이 있다. 패키지 구조를 봤을 때에 같은 order 영역에서도 repository를 나눈 것을 확인 할 수 있다.
- entity를 조회하거나 직접 저장하는 경우에는 일반적인 orderRepository를 쓴다.
- 화면에 따라 fit하게, 상황에 맞게 쿼리를 정의해야 하는 경우에는 따로 repository(예:OrderQueryRepository)를 만들어 관리한다.
장점 : 화면에 따라 보이는 것과 핵심 비지니스로직을 따로 관리할 수 있다.
DTO 직접 조회
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrders() {
return em.createQuery("select new jpabook2.jpashop2.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();
}
}
@Data
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
}
- DTO를 자세히 살펴보면 orderItems는 생성자에 들어 있지 않으며, repository 쿼리에도 포함되어있지 않다.
- orderItems는 일대다이며, DTO로 불러올 때에는 row수의 증가를 막기 위해 일단 플랫하게 일대다는 빼고 불러온다.
해결
package jpabook2.jpashop2.repository.order.query;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos(){
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook2.jpashop2.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count) " +
"from OrderItem oi " +
"join oi.item i " +
"where oi.order.id = :orderId", OrderItemQueryDto.class).setParameter("orderId",orderId).getResultList();
}
public List<OrderQueryDto> findOrders() {
return em.createQuery("select new jpabook2.jpashop2.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();
}
}
- 결국 일대다는 findOrderQuertDtos 메소드에서 보이듯이 orderItem을 먼저 가져온 후에 for문을 돌려 아이템을 각각 가져와 set해주는 방식으로 사용하게 된다.
- 하지만 for문을 사용하는 방식.. 그리고 1개의 쿼리에 대한 일대다 쿼리를 가져와야하는 1 + N의 문제를 가져왔다. 몇백개의 쿼리가 날아갈지 모르는 상황이다. 최적화가 필요하다.
최적화
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5(){
return orderQueryRepository.findAllByDto_optimization();
}
package jpabook2.jpashop2.repository.order.query;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos(){
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook2.jpashop2.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count) " +
"from OrderItem oi " +
"join oi.item i " +
"where oi.order.id = :orderId", OrderItemQueryDto.class).setParameter("orderId",orderId).getResultList();
}
public List<OrderQueryDto> findOrders() {
return em.createQuery("select new jpabook2.jpashop2.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();
}
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders();
List<Long> orderIds = result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds);
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
public Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds){
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook2.jpashop2.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())); // key값은 orderId가 되고 value는 OrderItemQueryDto가 된다.
return orderItemMap;
}
}
- 마지막 2개의 메소드만 주목해서 보면 된다.
- 먼저 findAllByDto_optimization()을 보면 findOrders로 일단 불러온다.
- 그 후에 orderId들을 묶은 리스트 orderIds를 사용해 쿼리문에 in 을 이용해서 한방쿼리로 사용한다.
- 그 뒤에 맵으로 key(id값) value 로 전환한 뒤에 for문을 사용해서 매칭 시켜서 객체에 담아준다.
- 코드 상으로는 복잡도가 증가하지만, 성능으로는 1방 쿼리로 가능하기에 많이 최적화 된다.
최적화2
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6(){
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(),
o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(),
o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(),
e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
package jpabook2.jpashop2.repository.order.query;
import jpabook2.jpashop2.domain.Address;
import jpabook2.jpashop2.domain.OrderStatus;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private String itemName;
private int orderPrice;
private int count;
public OrderFlatDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery("select new jpabook2.jpashop2.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i",OrderFlatDto.class).getResultList();
}
- 먼저 컨트롤러의 저 복잡한 코드를 요약하자면, flatDTO가 key값이 1의 dto가 되고, 그 키값으로 orderItem을 빼내오면서 키값도 같이 넣어 DTO를 불러온다. 이때 group by의 키 값의 dto에 작성한다.
package jpabook2.jpashop2.repository.order.query;
import jpabook2.jpashop2.domain.Address;
import jpabook2.jpashop2.domain.OrderStatus;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
@Data
@EqualsAndHashCode(of = "orderId")
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, List<OrderItemQueryDto> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
}
- EqualsAndHashCode(of = "orderId")
- 결국 불러온 flat한 데이터 (그냥 일대다 쿼리로 불러온거다) 지지고 볶아서 만들어 내는 방식
- 이 방법은 쿼리 한방에 처리된다. 하지만 join이 많기 때문에 데이터 양이 많으면 성능이 떨어질 수도 있다.
- 페이징을 할 수 없다.
- 딱히 쓰기에 좋은 방법은 아닌거 같다.
'dev > spring JPA 활용 웹만들기' 카테고리의 다른 글
OSIV와 성능 최적화 (0) | 2024.03.04 |
---|---|
JPA Api(7) 조회 권장 순서 (0) | 2024.03.04 |
JPA api (3) (성능최적화 2) (toOne) (조회시 DTO 변환) (0) | 2024.03.02 |
JPA API (2) (성능최적화 1)(toOne)(엔티티로 반환..근데 이건 쓰지말자) (0) | 2024.03.02 |
JPA API (1) DTO로 따로 관리해야한다. (등록,수정,조회) (0) | 2024.03.01 |
- Total
- Today
- Yesterday
- 로그인
- hypertexttransferprotocol
- 항해99
- filter
- 스프링부트
- 향해99
- thymleaf
- 컨트
- 향해플러스
- reject
- react실행
- 백엔드 개발자 공부
- 스프링공부
- 리터럴
- Intercepter
- rejectValue
- 예외처리
- Java
- HTTP
- ArgumentResolver
- 향해플러스백엔드
- BindingResult
- React
- 항해플러스
- 백엔드 개발자 역량
- jpa api
- exception
- SpringBoot
- 인터셉터
- JPA
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |