프로젝트

[SpringBoot & JPA 프로젝트] 주문테이블 설계

sian han 2022. 9. 5. 18:44

주문하기는 모든 테이블과 연관되어있어서 마지막으로 남겨뒀다.

 

주문하기 테이블 설계에 대한 고민

 

 

1. 장바구니를 구현한것과 동일하게 구현

Cart - CartItem 테이블을 만들어서 장바구니 기능을 구현한 것 처럼

Order - OrderItem 테이블을 만드는 방법.

 

'주문하기' 버튼을 누르면 Order-OrderItem 테이블에 정보가 저장되고

이를 결제 전 최종확인 단계에서 볼 수 있음.

결제가 완료되면 결제여부를 알려주는 컬럼의 값이 변경됨.

 

But 나는 Order 테이블에 결제 완료된 애들만 저장하고 싶은데 ? => 일단보류.

 

 

 

2. 상품번호를 배열에 저장해 파라미터로 넘기는 방법

 

결제 전 최종확인 단계에서 주문정보를 보여줘야 함.

이 때 보여줄 정보는 상품번호를 배열로 받아서 findById 메서드를 이용해 List<DTO> 형태로 view 로 보내서

사용자가 결제 전 주문정보를 확인할 수 있도록 함.

결제가 완료되면 Order 테이블에 저장.

 

그럼 결제 시

컨트롤러에 또 상품번호 배열을 파라미터로 넘겨서 findById 메서드를 이용해서 DB에 저장해야하는건가 ?

 => 같은 동작이 반복됨. 탐탁치 않음.

결제하기 동작 실행은 1번과 동일하게 진행됨. 차라리 1번 방법으로 가는게 낮겠음. 


 

※ Entity 설계

 

Order

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "orders") //중요 ! 
@Entity
public class Order extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name ="order_id")
    private Long id; //주문번호

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.CANCEL; //주문상태

    @OneToMany(mappedBy = "order",fetch = FetchType.LAZY)
    private List<OrderItem> orderItems = new ArrayList<>();

    private int totalPrice;

    @Builder
    public Order(User user, OrderStatus orderStatus,List<OrderItem>orderItems,int totalPrice) {
        this.user = user;
        this.orderStatus = orderStatus;
        this.orderItems=orderItems;
        this.totalPrice = totalPrice;
    }
}

 

 

OrderItem

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "order_item")
@Entity
public class OrderItem extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name ="order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="order_id")
    private Order order;

    @Builder
    public OrderItem(Long id,Item item, Order order) {
        this.id = id;
        this.item = item;
        this.order = order;
    }
}

 

 


※ 단일상품주문

Service

//단일상품주문
    @Transactional
     public Long order(Long itemId,String email){
         //orderItem 객체생성
         List<OrderItem> orderItems = new ArrayList<>();
         Item item = itemRepository.findItemById(itemId);
         log.info("itmeId = {}",itemId);
         OrderItemDto orderItemDto = OrderItemDto.builder()
                 .item(item)
                 .build();

         orderItems.add(orderItemDto.toEntity());

         //order 객체생성
         User user = userRepository.findByEmail(email);
         log.info("userName = {}",user.getName());
         OrderDto orderDto = OrderDto.builder()
                 .user(user)
                 .orderStatus(OrderStatus.CANCEL)
                 .orderItems(orderItems)
                 .build();

         //order 객체 db저장 (Cascade로 인해 OrderItem 객체도 같이 저장)
         Order order = orderRepository.save(orderDto.toEntity());

         //item SellStatus 변경
         item.updateSellStatus(SellStatus.SOLD_OUT);
         itemRepository.save(item);

         log.info("orderId = {}",order.getId());

        return order.getId();
     }

 

메서드를 너무 한개에 때려넣었나 .. 싶어서 장바구니 상품 주문 메서드 구현할때는 좀 쪼갰다 ㅎㅅㅎ

 


※ 장바구니 상품주문

오류 : TransientPropertyValueException

object references an unsaved transient instance - save the transient instance before flushing

 

 

JPA 관련 Hibernate 에러: object references an unsaved transient instance - save the transient instance before flushing

에러 로그 TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing 원인 JPA 연관 관계 테스트 중에 발생했습니다. FK 로 사용되는..

bcp0109.tistory.com

위 블로그에 따르면 해당 에러는

FK로 사용되는 컬럼값이 없는 상태에서 데이터를 넣으려다 발생한 에러 ! 라고한다

 

작성한 Service 코드를 뜯어보자

//장바구니 상품을 주문상품에 넣어주는 메서드
    @Transactional
    public OrderItemDto addOrderItem(Item item,Order order){
        OrderItemDto orderItemDto = OrderItemDto.builder()
                .item(item)
                .order(order)
                .build();

        orderItemRepository.save(orderItemDto.toEntity());
        return orderItemDto;
    }

    //주문상품 목록을 주문에 넣어주는 메서드
    @Transactional
    public void addOrder(List<OrderItem> orderItems,Order order){
       order = Order.builder()
               .orderItems(orderItems)
               .build();
       orderRepository.save(order);
    }
 //장바구니 상품 전체 주문
    @Transactional
    public int orders(Long userId){
        User user = userRepository.findById(userId).orElseThrow(EntityNotFoundException::new);
        //User정보를 가진 Order 객체 생성
        Order order = Order.createOrder(user);
        int result = 0;

        //user의 장바구니와 장바구니 속 상품 목록 가져오기
        Cart cart = cartService.findUserCart(user.getId());
        List<CartItemDto> cartItems = cartService.getCartItemList(user.getId());

        int totalPrice = 0;
        List<OrderItem> orderItems = new ArrayList<>();

        for(CartItemDto cartItemDto : cartItems){

            // 이미 판매된 상품이 장바구니에 포함되어있으면 result = 1
            if(cartItemDto.getItem().getSellStatus()!=SellStatus.SELL){
                result= 1;
                return result;
            }
            totalPrice += cartItemDto.getItem().getPrice();

            //Item 을 주문상품(OrderItem) 에 넣어줌
            OrderItemDto orderItemDto = addOrderItem(cartItemDto.getItem(), order);
            orderItems.add(orderItemDto.toEntity()); //주문상품들을 주문상품 목록에 추가
        }

        //생성된 order 객체에 orderItems 정보 넣기
        addOrder(orderItems,order);
        return result;
    }

 

 

오류 1.  [장바구니 상품을 주문상품에 넣어주는 메서드]  addOrderItem

 

리턴값은 id가 아직 생성되지않은 orderItemDto 가 아니라

orderItemDto 를 save 하고 반환된 orderItem 객체를 리턴하는게 옳다.

 //수정전
    @Transactional
    public OrderItemDto addOrderItem(Item item,Order order){
        OrderItemDto orderItemDto = OrderItemDto.builder()
                .item(item)
                .order(order)
                .build();

        orderItemRepository.save(orderItemDto.toEntity());
        return orderItemDto;
    }
    
    //수정후
    @Transactional
    public OrderItem addOrderItem(Item item,Order order){
        OrderItemDto orderItemDto = OrderItemDto.builder()
                .item(item)
                .order(order)
                .build();

        OrderItem orderItem =  orderItemRepository.save(orderItemDto.toEntity());
        return orderItem;
    }

 

 

오류 2.  createOrder 메서드

Order 객체만 생성하고 save 를 안해주니 fk 가 생성되지 않아서 오류가 발생했다

정확히는 이 부분에서 오류가 난건데

OrderItemDto orderItemDto = addOrderItem(cartItemDto.getItem(), order);
addOrder(orderItems,order);

이때 order 객체는 id 를 가지고있지않기 때문에 addOrderItem에서

OrderItem 을 save 하는 메서드에서 order 의 fk 가 없어서 오류가 발생한 것이다.

 

 

 

수정 전 createOrder

public static Order createOrder(User user) {
        Order order = new Order();
        
        OrderDto orderDto = OrderDto.builder()
        .user(user)
        .build();
        return orderDto.toEntity();
    }

(다 지워서 잘 기억안나는데 이런식으로 만들었었던것같음)

 

 

수정 후

public Order createOrder(User user){
        OrderDto orderDto = OrderDto.builder()
                .user(user)
                .build();
       return orderRepository.save(orderDto.toEntity());
    }

 

 

 

결과 : 실패

사유 : insert 쿼리가 두번날라감

JPA 의 DirtyChecking 을 통해 save 메서드를 이용하면

변경을 감지하여 자동으로 update 쿼리가 날라가는 것이 아닌가 ? 

첫번째 save() = insert 

두번째 save() = update 

쿼리가 보내졌어야하는데 왜 두번 다 insert 로 처리되었을까 ? 테스트 코드 작성을 통해 확인해보자

 

 @Test
    @DisplayName("주문정보 입력테스트")
    void test2(){
        //given
        Long userId = 3L;

        //when
        int result = orderService.orders(userId);

        //then
        assertEquals(1L, orderRepository.count());

 

확실히 insert 쿼리가 두번 날라가서 expected result 갯수가 다르다.

 

save() 는 id로 insert / update 를 결정한다.

두번째 save() 시 builder 로 id 를 값을 넣고, update 될 다른 값들을 추가로 넣어줬다.

그렇다면 builder 에서 id 가 제대로 빌드되지않아 insert 가 두번된걸까 ?

==> YES !!

 

OrderDto Builder에 id를 누락했었다.

id 가 빌드되지않았으니 없는 객체로 인식하여 insert 쿼리를 날리고 있었던 것이다.

해서 수정함.

 public Order toEntity(){
        return Order.builder()
                .id(id)
                .user(user)
                .orderItems(orderItems)
                .orderStatus(orderStatus)
                .totalPrice(totalPrice)
                .build();
    }

    @Builder
    public OrderDto(Long id, OrderStatus orderStatus,User user, List<OrderItem> orderItems,int totalPrice){
        this.id = id;
        this.user = user;
        this.orderStatus = orderStatus;
        this.orderItems = orderItems;
        this.totalPrice = totalPrice;
    }
//주문정보 입력해주는 메서드
    @Transactional
    public Order addOrderInfo(List<OrderItem> orderItems,Long orderId,int totalPrice){
        Order order = orderRepository.findOrderById(orderId);

        OrderDto orderDto = OrderDto.builder()
                .id(order.getId())
                .user(order.getUser())
                .orderItems(orderItems)
                .orderStatus(OrderStatus.CANCEL)
                .totalPrice(totalPrice)
                .build();

       return orderRepository.save(orderDto.toEntity());
    }

TEST 성공 ㅎㅎ

 


※ 마지막으로 구매목록에서 상품명 (00외 N개) 설정하기

 

 

OrderDto 에 주문 이름 (00외 n개) 멤버 추가

뷰에서만 확인하면 되는 정보이기 때문에 entity 에는 따로 주가 하지 않고

뷰에서 응답받는 OrderDto 에만 멤버변수 추가해줌

    private String orderName;

 

 

각 주문의 상품개수를 조회하여 orderName을 만들어

orderDto 로 리턴해주는 메서드 selectOrderList 생성 (진짜 재밌게 만든 메서드 ㅎㅎ)

//사용자의 주문목록 조회
    @Transactional
    public List<OrderDto> selectOrderList(Long userId){
        List<Order> orderList = orderRepository.findOrdersByUser_IdAndOrderStatus(userId, OrderStatus.ORDER);
        List <OrderDto> orderDtoList = new ArrayList<>();

        int itemQty = 0;
        String orderName = "";
        OrderDto orderDto = new OrderDto();

        for(Order order : orderList){ //사용자의 주문갯수만큼
            List<OrderItem> orderItemList = order.getOrderItems();

            for(OrderItem orderItem : orderItemList){ //주문1개의 상품개수만큼
                itemQty ++;
                orderName = orderItem.getItem().getItemName();
            }

            if(itemQty>1){
                itemQty -= 1;
                orderName = orderName + " 외 " + itemQty +"개";
            }

            orderDto = OrderDto.builder()
                    .id(order.getId())
                    .orderName(orderName)
                    .totalPrice(order.getTotalPrice())
                    .regTime(order.getRegTime())
                    .build();

            orderDtoList.add(orderDto);
        }
        return orderDtoList;
    }