1:다 조회를 해볼 것
-> 주문 내역에서 추가로 주문한 상품 정보 추가로 조회하기
-> Order기준으로 컬렉션인 OrderItem 과 Item 필요
-> 이땐 최적화가 어려워지는게 컬렉션이어서 디비 입장에서 데이터 뻥튀기된 느낌!
-> 보면 오더아이템이 리스트로 되어있음!
꿀팁 : iter 하고 tab하면 인텔리제이가 자동으로 포문변경해줌
private final OrderRepository orderRepository;
@GetMapping("/api/v1/orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
//다 한번에 뿌리면 문제 생겨서 지연로딩 터치해주기
for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems) {
//각 오더 아이템의 이름도 초기화시켜주기
orderItem.getItem().getName();
}
}
return all;
}
-> 엔티티를 직접 노출하기 때문에 좋지 않은 방법!
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
//orders 를 orderDto로 변환
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
//내부 객체 생성
static class OrderDto{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
//전에비해 추가된 부분
private List<OrderItem> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
order.getOrderItems().stream().forEach(o->o.getItem().getName());
orderItems = order.getOrderItems();
}
}
근데 사실 DTO로 변환하라 했을 때는 DTO가 wrapping 해서도 안되고 안에 entity가 있어도 안돼!
-> 왜냐면 orderItems 의스펙이 다 외부에 노출이 되기 때문!
-> 엔티티 노출 시키지 말라는게 이렇게 DTO로 한번 감싸지말라는게 아니라 엔티티에 대한 의존을 완전히 끊으라는 뜻!
-> 그래서 orderItem에 대한 DTO도 새로 만들어 줘야해 !
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
//orders 를 orderDto로 변환
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
//내부 객체 생성
@Getter
static class OrderDto{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
//전에비해 추가된 부분
private List<OrderItemDto> orderItems;
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(Collectors.toList());
}
}
//OrderItem을 위한 DTO
@Getter
static class OrderItemDto{
//생성자에서 orderItem안에서 내가 노출하고 싶은 부분을 결정하기
//내가 상품명만 필요하다면 그 정보만 노출시키는것!
//클라이언트의 요구사항에 맞게 짜면되는거
private String itemName; //상품명
private int orderPrice; //주문가격
private int count; //주문 수량
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
//이렇게하면 외부로는 내가 원하는 정보만 보여줄수있어
}
-> 원하는 정보만 나옴!
껍데기뿐만이 아니라 속에 있는거까지 엔티티를 외부에 노출해선 안된다는 것!!!!!!!
-> 내가 원하는 정보만 노출하자!
//매핑 코드
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
//OrderRepository에 만든 함수
public List<Order> findAllWithItem() {
//실무에선느 query ds 로 훨씬 편하게 짤 수 있다!
//기존이랑 똑같은데 order이랑 orderitems를 조인해
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
-> 이렇게하면 조인을 하면서 데이터가 뻥튀기돼서 각 아이디 별 order 가 2개씩 나오게돼,,!
-> 우리가 의도한거랑 다른 결과물이 나오게 된 것임
-> 우리가 뻥튀기된것에 대한 기준을 제시해줘야해. Order에 대해서
-> 디비가 1:다 관계에서 다 에 맞춰서 즉, order 에 맞춰서 뻥튀기가 돼서!
-> 따라서 sql문 앞에 select distinct, 즉 distinct 키워드 추가
-> 이 distinct 키워드는 SQL에서의 distinct랑 다른점이 sql은 모든 속성값이 일치해야 중복처리 되는데, 여기서는 JPA에서 자체적으로 id값이 같으면 중복이라고 보고 중복처리를 함
-> 이렇게하면 쿼리가 1번 나가! 패치조인으로 인해!
이전에 패치조인을 배웠는데, 컬렉션에서 왜 또 언급할까?
1대다에서는 패치조인하면 페이징이 불가능해 !!!!!!!
-> 디비에서 몇번째부터 몇개 가져와~~ 이런거임!
-> 패치조인을 하면 sql에서 페이징을 위한 limit를 안써서 안되는것!
-> 뻥튀기된결과물 기준으로해서 안하게 되는것임
즉, row수를 증가시키는 상황에 페이징이 안되는것!
1) ToOne(OneToOne, ManyToOne) 관계를 모두 패치조인함
-> ToOne 관게는 Row수를 증가시키지 않으므로 페이징 쿼리에 영향 주지 않음
-> 오더 입장에서 맴버랑 딜리버리
2) 컬렉션은 지연로딩으로 조회
3) 지연 로딩 성능 최적화를 위해 'hibernate.default_batch_fetch_size', '@BatchSize'를 적용한다
-> hibernate.default_batch_fetch_size : 글로벌 설정
ㄴ 웬만해선 켜두는게좋아
ㄴ 적어놓은 개수만큼 미리 가져오는 친구
-> @BatchSize : 개별 최적화
-> 이 옵션들을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 사이즈만큼 IN 쿼리로 조회함
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page( {
//order,member,delivery 페치조인하는 함수 쓴거임
//toOne관계여서 패치조인해서 가져옴 !
//쿼리한번사용
List<Order> orders = orderRepository.findAllWithMemberDelivery();
//orderitem은 컬렉션 조회할 때 아이템 개수가 2개인데 그 안에는 각각 아이템 2개가 있어
//레이지로딩 걸려서 아이템 그때 조회돼서 가져와져
//그래서 쿼리가 총 6번 실행돼. orderitem+각아이템2개 * 2 -> 1+N+N
//오더가 많다면 성능이 안나올것!
//그래서 @RequestParam(value = "offset", defaultVaule = 0 ...등등 옵션추가)
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
//페이징 위한 매개변수 사용 코드
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();
}
-> 이렇게하면 페이징 잘할수이씀
-> 오프셋 1로해서 첫번째꺼 날리고 두번째꺼만 보여줌
-> 오프셋 0이면 오프셋 없는것처럼 행동
이걸 쉽게하는게 application.yml에 속성 추가하기!
-> 이렇게하면 쿼리문이 in 키워드를 써서 날아감
-> 한번에 in 쿼리로 디비에 있는 orderItem을 한번에 가져온 것임
-> 컬렉션과 관련된 애들을 jpa가 한번에 가져온거
-> 100이란 숫자가 in 쿼리에 들어간 애들 숫자 설정한거
-> 만약 데이터 100갠데 사이즈 10이면 10번 sql문 실행되는것
쿼리1개로 다 해결되는 것!
-> 1+N+N이 1+1+1이됨
-> 근데 사실 쿼리는 한번이지만 디비가 어플리케이션에 전체 다 전송하는거라 용량이 커진다는 이슈가 있음.
+ 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 감소하지만, DB 데이터 전송량이 증가한다.
+ 컬렉션 페치 조인은 페이징이 불가능 하지만 이 방법은 페이징이 가능하다.
-> 페이징이 필요할 땐 이 방식으로 쓰고 ToOne관계는 패치조인해도 문제 없으니까 그냥하기!
-> ToOne관계도 저 설정 영향받아서 최적화됨
@BatchSize
-> 각 속성위에 적어서 개별적으로 설정 가능한데, 굳이 ? 그냥 application에 추가해서 쓰는거로 하라고 한다!