본문 바로가기
스프링 쇼핑몰 만들어보기

주문, 주문 목록 만들기 (@ManyToOne, @OneToMany, N+1 문제)

by hoshi03 2024. 5. 13.

주문을 하는 기능과 주문 목록을 저장하는 기능을 만들어보자

 

주문 id, 주문한 상품의 이름, 가격, 갯수와 주문한 사람의 정보를 저장했다

정보는 @ManyToOne, @JoinColumn 해서 가져왔다

@Entity
@Getter
@Setter
@ToString
public class Sales {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column
    private String itemName;
    @Column
    private Integer price;
    @Column
    private Integer count;
    @ManyToOne
    @JoinColumn(name = "member_id",
            foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)
    )
    private Member member;
    @CreationTimestamp
    private LocalDateTime created;
}

 

주문 컨트롤러를 만들고 주문 기능과 주문 목록 기능을 테스트해 봤다

@PreAuthorize("isAuthenticated()")
    @PostMapping("/order")
    String postSales(@RequestParam Integer count, @RequestParam Integer price,
                       @RequestParam String itemName, Model model , Authentication auth){
        CustomUser user = (CustomUser) auth.getPrincipal(); //로그인 된 사용자 정보 가져오기!
        Sales sales = new Sales();
        sales.setItemName(itemName);
        sales.setPrice(price);
        Member member = new Member();
        member.setId(user.userId);
        sales.setMember(member);
        sales.setCount(count);
        salesRepository.save(sales);
/*        MemberDto memberInfo = memberService.getMemberInfo(member.get().getId());
        model.addAttribute("memberInfo", memberInfo);*/
        return "redirect:/salesList";
    }

    @GetMapping("/salesList")
    String salesList(Model model){
        List<Sales> salesList = salesRepository.findAll();
        System.out.println(salesList);
        return "salesList.html";
    }

 

• N+1 문제

주문은 잘 저장됬는데 문제가 있다

 

전체 조회 쿼리, 멤버 데이터 조회 쿼리(멤버 숫자만큼 발생) select를 하기에 상당히  비효율적이다

이게 N+1 문제라고 한다

해결하기위해 JPQL을 사용한 쿼리문을 작성한다

native sql로 조인을 사용해서 select * from Sales s , Member m where s.member_id = m.id; 이런식으로 작성해도 

jpa가 번역하는 과정에서 N+1 문제가 발생하게 된다..

그래서 JPQL을 사용해서 리포지토리에 아래처럼 코드를 작성하면 n+1문제를 해결할 수 있다!

@Repository
public interface SalesRepository extends JpaRepository<Sales,Long> {
    @Query(value = "SELECT s FROM Sales s JOIN FETCH s.member")
    List<Sales> customFindAll();
}

 

• dto 사용

그리고 이대로 member를 보냈다간 restapi로 보내면 유저 비밀 번호까지 보내버리는 대참사가 일어날 수 있다

dto를 사용해서 보내고 싶은 데이터만 걸러서 보내자

 

보내고 싶은 정보인 구매한 아이템 이름, 가격, 구매자 id, 구매시간, 구매수량, 총 가격을 보낼 dto를 만들고

@Getter
@Setter
@ToString
@NoArgsConstructor
public class SalesDto {
    private String itemName;
    private Integer price;
    private String userName;
    private LocalDateTime created;
    private Integer count;
    private Integer sum;
}

 

컨트롤러에서는 sales를 그대로 보내지 않게 가져온걸 salesDto에 담아서 보내준다

@GetMapping("/salesList")
String salesList(Model model) {
    List<Sales> salesList = salesRepository.customFindAll();
    List<SalesDto> salesDtoList = new ArrayList<>();

    for (Sales sales : salesList) {
        SalesDto salesDto = new SalesDto();
        salesDto.setItemName(sales.getItemName());
        salesDto.setPrice(sales.getPrice());
        salesDto.setCreated(sales.getCreated());
        salesDto.setCount(sales.getCount());
        salesDto.setSum(sales.getCount() * salesDto.getPrice());
        salesDto.setUserName(sales.getMember().getUserName());
        salesDtoList.add(salesDto);
    }

    model.addAttribute("salesList", salesDtoList);

    return "salesList";
}

 

salesList.html에는 기존에 list에 한것처럼 th:each로 각각 dto의 데이터를 뽑아서 보여준다

<div class="card" th:each = "i : ${salesList}">
    <div>
        <h4 th:text="'구매자 이름 : ' + ${i.userName}"></h4>
        <h4 th:text="'주문 상품 이름 : ' + ${i.itemName}"></h4>
        <h4 th:text="'주문 상품 가격 : ' + ${i.price}"></h4>
        <h4 th:text="'주문 수량 : ' + ${i.count}"></h4>
        <h4 th:text="'총 가격 : ' + ${i.sum}"></h4>
        <h4 th:text="'주문 일시 : ' +${i.created}"></h4>
    </div>
</div>

원했던 대로 잘 나온다

 

• @OneToMany

 

지금은 sales에 있는 member_id로 해당하는 유저 member를 가져온다

특정 유저가 한 주문을 보려면?

 

Member 엔티티에 아래와 같은 컬럼을 추가한다, mappedBy는 Sales에 @ManyToOne을 한 컬럼을 넣어준다

@OneToMany(mappedBy = "member") 
List<Sales> salesList = new ArrayList<>();

 

• @OneToMany, @ToString stackOverFlow 에러

 

아래 코드로 이대로 조회해보려고 하니 stackOverFlow 에러가 떳다

이유는 @ToString과 @OneToMany를 같이 사용할 시 상호참조로 무한루프가 뜰 수 있다고 한다

 

@OneToMany 부분 위에 ToString을 빼는 어노테이션을 달면 해결 가능하다

@ToString.Exclude
@OneToMany(mappedBy = "member")
List<Sales> salesList = new ArrayList<>();

 

• @OneToMany 조회

특정 멤버 가져온 것 에서 위의 salesList를 get 하면 해당 멤버 id의 주문 내역이 쭉 나온다

@GetMapping("/order/all")
    String getMember(){
        var member = memberRepository.findById(3L);
        System.out.println(member.get().getSalesList());
        return "index.html";
    }

 

• @ManyToOne @OneToMany 뭘써야되나

지금 db 구조에서는 member 하나가 여러개의 sales를 가질 수 있음

- member 에 oneToMany로 sales에서 가져다 쓰는 컬럼 이름을 쓰면 됨

근데 뭐 사실 특정 멤버 주문 내역을 보고 싶으면

salesRepository에 findbyMemberId 해서 가져오면 되기도 해서 아직은 언제 써야 되는지 잘 모르겟지만 하다보면 알게되고 지금 이렇게 갈기는 것도 나중에 내가 블로그 리팩토링 하면서 지우겟죠? 그러길 바랍니다

 

• 요약

2 정규화로 sales에 member 정보 다 있는 걸 meberId만 들고 있게 했고

manyToOne으로 해당 컬럼(memberId)가 가르키는 컬럼 출력 , manyToOne 성능 문제는 JPQL join fetch로 해결

@OneTomany로 반대로도 가능 

 

꼭 manyToOne 할때 상대방에 OneToMany 할 필요는 없지만 붙이면 테이블 구조 파악할때 도움이 된다고 하고 관련된 행을 삭제할때도 도움이 된다고 합니다