티스토리 뷰

카테고리 없음

JPA Api (4) (toMany)

dev_0hoon 2024. 3. 2. 15:42
package jpabook2.jpashop2.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name= "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) //order 입장에서는 여러개의 order에 하나의 회원이다.
    @JoinColumn(name = "member_id") //FK를 지정해준다.
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) //cascade는 여러개를 한 번에 해줌.. 무슨말인지 잘모르겠다
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태 [ORDER, CANCEL]

    //==연관관계 메서드==//
    public void setMember(Member member){
        this.member =member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public void setDelivery(Delivery delivery){
        this.delivery = delivery;
        delivery.setOrder(this);
    }

    //==생성 메서드==//
    public static Order createOrder(Member member,Delivery delivery, OrderItem... orderItems){
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for(OrderItem orderItem : orderItems){
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());

        return order;
    }
    //==비지니스 로직 ==//
    /**
     * 주문 취소
     */
    public void cancel(){
        if(delivery.getStatus() == DeliveryStatus.COMP){
            throw new IllegalStateException("이미 배송완료 된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCEL);
        for(OrderItem orderItem : orderItems){
            orderItem.cancel();
        }
    }
    //==조회 로직==//
    /**
     * 전체 주문 가격 조회
     */
    public int getTotalPrice(){
        /*
        int totalPrice = 0;
        for(OrderItem orderItem : orderItems){
            totalPrice += orderItem.getTotalPrice();
        }
        */
        return orderItems.stream()
                .mapToInt(OrderItem::getTotalPrice)
                .sum();
    }
}

 

order 객체의 일대다 관계인 orderItems을 꺼내보려한다.

 

V1

    /*엔티티 그대로 노출*/
    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1(){
        List<Order> all = orderRepository.findAllByString(new OrderSearch());

        for (Order order : all) {
            order.getMember().getName();
            order.getDelivery().getStatus();
            List<OrderItem> orderItems = order.getOrderItems();
            orderItems.stream().forEach(o -> o.getItem().getName()); //자동 초기화
        }

        return all;
    }

 

- 조회 후에 반복문으로 LAZY 부분을 강제 초기화 시켰다.

- 엔티티가 그대로 노출된다. 바꿔야 한다.

 

V2

    @GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2(){
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<OrderDto> collect = orders.stream().map(o -> new OrderDto(o)).collect(Collectors.toList());

        return collect;
    }

    @Data
    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();
            orderItems = order.getOrderItems();
        }

    }

 

- 이렇게 하면 DTO로 사용하게 된다. 하지만 DTO를 잘보면 OrderItem은 엔티티다. 이렇게 사용하면 또 문제가 된다. 엔티티를 그대로 사용하는 것으로 api스펙이 바뀌면 문제가 또 된다. 이것마저 DTO로 변경해야한다.

    @GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2(){
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<OrderDto> result = orders.stream().map(o -> new OrderDto(o)).collect(Collectors.toList());

        return result;
    }

    @Data
    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());
        }

    }

    @Getter
    static class OrderItemDto{
        private String itemName;
        private int orderPrice;
        private int count;

        public OrderItemDto(OrderItem orderItem){
            itemName = orderItem.getItem().getName();
            orderPrice = orderItem.getOrderPrice();
            count = orderItem.getCount();
        }
    }

이렇게 하면 해결된다. 하지만 LAZY로 인해 아직 조회 쿼리가 많이 날아간다..

 

V3

    @GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3(){
        List<Order> orders = orderRepository.findAllWithItem();

        for (Order order : orders) {
            System.out.println("order ref = " + order + " id = " + order.getId());
        }

        List<OrderDto> result = orders.stream().map(o -> new OrderDto(o)).collect(Collectors.toList());

        return result;
    }
    public List<Order> findAllWithItem() {
        return em.createQuery(
                "select distinct 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();

    }

 

레퍼지토리에 fetch join을 해주었다. 실제로 쿼리를 조인하면 일대다 관계의 경우

- 일의 데이터가 1개 

- 다의 데이터가 2개

그럼 조회 시에 row는 2개가 된다.

  • JPA api 안에서 데이터 또한 객체가 2개 반환된다. 그걸 json으로 반환하면 원하는 값을 주는게 아니다.
  • 우리는 일의 데이터 1개 안에 다의 데이터 2개를 원한다.
  • 쿼리에 distinct를 붙여주면 자동으로 중복이 제거되면서 원하는 데이터를 얻을 수 있다.
  • 하이버네이트v6 이후부터는 distinct없이도 중복이 제거된다

fetch join은 다대일에서도 똑같이 적용했는데 굳이 차이없는 것이 아닌가..?

일대다 fetch join시 장단점

  • distinct로 인해 fetchjoin때문에 오는 중복 조회 되는 것을 막아준다.
  • 큰 단점 : 페이징이 불가능 하다.
    public List<Order> findAllWithItem() {
        return em.createQuery(
                "select distinct 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)
                .setFirstResult(1)
                .setMaxResults(100)
                .getResultList();

    }

 

select distinct o1_0.order_id,d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status,m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name,o1_0.order_date,oi1_0.order_id,oi1_0.order_item_id,oi1_0.count,i1_0.item_id,i1_0.dtype,i1_0.name,i1_0.price,i1_0.stock_quantity,i1_0.artist,i1_0.etc,i1_0.author,i1_0.isbn,i1_0.actor,i1_0.director,oi1_0.order_price,o1_0.status from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id join order_item oi1_0 on o1_0.order_id=oi1_0.order_id join item i1_0 on i1_0.item_id=oi1_0.item_id

쿼리 안에 limit가 없다. 오잉? 근데 1번 데이터 부터 불러와 진다.

 

로그를 보면 warm 메세지가 있다.

2024-03-02T15:34:11.510+09:00  WARN 3969 --- [nio-8080-exec-2] org.hibernate.orm.query                  : HHH90003004: 
firstResult/maxResults specified with collection fetch; applying in memory

- 페치조인을 쓰는데 메모리에서 페이징처리 해버릴거야(만약 데이터 10000개가 있으면 메모리가 out 되어버린다.)

 

한마디로 쿼리에서 페이징이 되는 것이 아니라 데이터를 모두 불러온 후에 애플리케이션 안에서 나눠버린다.

- 이 전에 데이터를 보면 4개 짜리 먼저 불러온 후에 jpa안에서 중복처리를 해주었다. 그러므로 데이터를 쿼리 안에서 limit 처리 할 수 없었던 것

- 일대다가 아니면 상관없다.

+ 컬렉션 패치 조인(일대다)는 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
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 31
글 보관함