목차
- 📌주문, 주문상품 엔티티 개발
- 📌주문 리포지토리 개발
- 📌주문 서비스 개발
- 📌주문 기능 테스트
- 📌주문 검색 기능 개발
📌주문, 주문상품 엔티티 개발
주문 엔티티 코드
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.*;
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
@JoinColumn(name = "member_id") //포링키 이름
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
//cascade = CascadeType.ALL = order를 persist 할때 자동으로 orderItems도 저장한다.
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivety_id")
private Delivery delivery;
private LocalDateTime orderData; //주문시간
@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.setOrderData(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.getOrderPrice();
}
return totalPrice;
}
}
기능(메서드)설명
- createOrder() : 생성메서드 , 주문 엔티티를 생성할 때 사용한다. 주문 회원,배송정보, 주문상품의 정보를 파라미터로 받아서 실제 주문 엔티티를 생성한다.
- cancel() : 주문취소메서드, 주문 취소시 사용한다.
- getTotalPrice() : 전체 주문 가격 조회, 주문 시 사용한 전체 주문 가격을 조회한다. 전체 주문 가격을 알려면 각각의 주문상품 가격을 알아야하기떄문에 로직은 orderItems를 포문으로 하나씩 orderItem에 받아서 하나씩 더한 값을 반환한다 (실무에서는 주로 주문에 전체 주문 가격 필드를 두고 역정규화 한다.)
주문상품 엔티티 코드
package jpabook.jpashop.domain;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
@Entity
@Getter
@Setter
public class OrderItem {
@Id @GeneratedValue
@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;
private int orderPrice; // 주문가격
private int count;
//생성 메서드
public static OrderItem createOrderItem(Item item, int orderPrice, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
//비즈니스 로직
public void cancel(){
getItem().addStock(count);
}
//조회로직
public int getTotalPrice(){
return getOrderPrice() * getCount();
}
}
기능(메서드)설명
- createOrderItem() : 생성메서드, 주문상품,가격,수량을 파라미터로 받아서 엔티티를 생성한다.
그리고 item.removeStock(count)를 호출해서 주문한 수량만큼 상품의 재고를 줄인다. - cancel() : 주문취소,취소한 주문 수량 만큼 상품의 재고를 늘린다.
- getTotalPrice() : 주문 가격 조회,주문가격과 상품수를 곱해서 반환한다.
📌주문 리포지토리 개발
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public void save(Order order) {
em.persist(order);
}
public Order findOne(Long id) {
return em.find(Order.class, id);
}
// public List<order> findAll(OrderSearch orderSearch){}
}
- 주문 리포지토리에는 주문엔티티를 저장하고 단건검색기능이 있다.
- findAll(OrderSearch orderSearch) 메서드는 나중에 추가하도록 한다.
📌주문 서비스 개발
주문 서비스 코드
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Delivery;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.repository.MemberRepository;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
//주문
@Transactional
public Long order(Long memberId, Long itemId, int count) {
//엔티티 조회
Member member = memberRepository.findOne(memberId);
Item item = itemRepository.findOne(itemId);
//배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
//주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
//주문 저장
orderRepository.save(order);
return order.getId();
}
//주문취소
@Transactional
public void cancelOrder(Long orderId) {
//주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel();
}
//검색
/*public List<Order> findOrders(OrderSearch orderSearch){
return orderRepository.findAll(orderSearch);
*/
}
주문 서비스 기능
- 주문, 주문취소,주문내역 검색
예제의 단후화를 위해서 한 번에 하나의 상품만 주문할 수 있다.
- order() : 주문, 주문하는 회원의 Id, 상품Id ,주문 수량을 파라미터로 받아서 실제 주문 엔티티를 생성한 후 저장한다.
- cancelOrder() : 주문Id를 받아서 주문 엔티티를 조회 후 주문 엔티티에 주문취소 메서드를 호출한다.
- findOrders() : 주문검색, OrderSearch 라는 검색 조건을 가진 객체로 주문 엔티티를 검색한다. 구현은 주문 검색 기능 파트에서 추가한다.
도메인 모델 패턴, 트랜잭션 스크립트 패턴이란?
- 도메인 모델 패턴
- 비지니스 로직이 대부분 엔티티에있다. 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을한다. 이 처럼 엔티티가 비지니스 로직을 가지고 객체지향의 특성을 적극 활용하는 것을 도메인 모델패턴이라고한다.
- 트랜잭션 스크립트 패턴
- 반대로 트랜잭션 스크립트 패턴은 서비스계층에서 비지니스 로직을 처리하는 스타일을 스크립트 패턴이라고 한다.
- 반대로 트랜잭션 스크립트 패턴은 서비스계층에서 비지니스 로직을 처리하는 스타일을 스크립트 패턴이라고 한다.
📌주문 기능 테스트
테스트 요구사항
- 상품 주문을 성공해야 한다.
- 상품을 주문할 때 재고 수량을 초과하면 안 된다.
- 주문 취소를 성공해야 한다.
package jpabook.jpashop.service;
import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.exception.NotEnoughStockException;
import jpabook.jpashop.repository.OrderRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import static org.junit.Assert.*;
@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class OrderServiceTest {
@PersistenceContext
EntityManager em;
@Autowired OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception {
Member member = createMember();
Book book = createBook("삼국지", 10000, 100);
int orderCount = 2;
//w
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
System.out.println("orderId = " + orderId);
//t
Order getOrder = orderRepository.findOne(orderId);
assertEquals("상품 주문시 상태는 ORDER",OrderStatus.ORDER,getOrder.getStatus());
assertEquals("주문한 상품 종류 수가 정확해야 한다.",1, getOrder.getOrderItems().size());
assertEquals("주문가격은 가격 * 수량",10000,getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야한다.",98,book.getStockQuantity());
}
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception{
//g
Member member = createMember();
Book book = createBook("삼국지", 10000, 100);
int orderCount = 111;
//w
orderService.order(member.getId(), book.getId() ,orderCount);
//t
fail("재고 수량 부족 예외가 발생해야한다.");
}
@Test
public void 주문취소() throws Exception{
//g
Member member = createMember();
Book book = createBook("삼국지", 10000, 100);
int count = 10;
Long order = orderService.order(member.getId(), book.getId(), count);
//w
orderService.cancelOrder(order);
//t
Order getOrder = orderRepository.findOne(order);
assertEquals("주문 취소시 상태는 캔슬이다.",OrderStatus.CANCEL, getOrder.getStatus());
assertEquals("주문 취소된 상품은 그만큼 재고가 증가해야한다.", 100,book.getStockQuantity());
}
private Book createBook(String name, int price, int stockQuantity) {
Book book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
em.persist(book);
return book;
}
private Member createMember() {
Member member = new Member();
member.setName("승빈");
member.setAddress(new Address("서울", "역삼동", "13245"));
em.persist(member);
return member;
}
}
public void 상품주문()
- Member createMember() 와
Book createBook(String name, int price, int stockQuantity)생성 메서드를 만들 상품주문 테스트에 회원과 상품을 만들고 상품을 주문해 보아서 재고수량이 주문수량만클 줄었는지 검증한다.public void 상품주문_재고수량초과()
- @Test(expected = NotEnoughStockException.class)를 속성으로 추가해 재고가 부족할시 기대하는 예외가 발생한는지 테스트한다.
public void 주문취소()
- 주문취소시 주문상태를 확인하고 취소댄 상품이 재고가 다시 증가했는지 확인한다.
📌주문 검색 기능 개발
JPA에서 동적 쿼리를 해결하는 방안
👀 JPQL
- 쿼리를 문자로 생성하기 번거롭고, 실수로 인한 버그가 생길 가능성이 많다.
👀 JPA Criteria
- JPA 표준 스펙이지만 실무에서 사용하기에 유지보수성이 떨어지고 너무 복잡하다.
👀 Querydsl 라이브러리
- 위 두가지 방안의 해결책으로 사용된다.
검색 조건 파라미터 OrderSearch
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class OrderSearch {
private String memberName; //회원이름
private OrderStatus orderStatus; // 주문상태 [ORDER, CANCEL]
}
검색 추가 주문 리포지토리
package jpabook.jpashop.repository;
@Repository
public class OrderRepository {
@PersistenceContext
EntityManager em;
public void save(Order order) {
em.persist(order);
}
public Order findOne(Long id) {
return em.find(Order.class, id);
}
public List<Order> findAll(OrderSearch orderSearch) {
//... 검색 로직
}
}
findAll(OrderSearch orderSearch)메서드가 검색 조건에 동적으로 쿼리를 생성해 주문 엔티티를 조회한다.
👀 JPQL로 처리
public List<Order> findAllByString(OrderSearch orderSearch) {
//language=JPAQL
String jpql = "select o From Order o join o.member m";
boolean isFirstCondition = true;
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " o.status = :status";
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class)
.setMaxResults(1000); //최대 1000건
if (orderSearch.getOrderStatus() != null) {
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if (StringUtils.hasText(orderSearch.getMemberName())) {
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
👀 JPA Criteria로 처리
public List<Order> findAllByCriteria(OrderSearch orderSearch) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
List<Predicate> criteria = new ArrayList<>();
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
Predicate status = cb.equal(o.get("status"),
orderSearch.getOrderStatus());
criteria.add(status);
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
Predicate name =
cb.like(m.<String>get("name"), "%" +
orderSearch.getMemberName() + "%");
criteria.add(name);
}
cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대
1000건
return query.getResultList();
}
👍정리
JPA 에서 동적 쿼리를 처리하는 방법이 JPQL,JPA Criteria와 같이 처리하는 방법이 있지만 이 두가지방법은 여러 문제점과 유지보수성이 떨어지며 복잡하다는 단점으로 이에 대한 해결책으로 Querydsl라이브러리를 사용한다.
REF
'강의 > 스프링부트 JPA활용 1' 카테고리의 다른 글
스프링부트 JPA활용 2 - 웹 계층 개발(7) (0) | 2022.06.17 |
---|---|
스프링부트 JPA활용 1 - 상품 도메인 개발(5) (0) | 2022.06.10 |
스프링부트 JPA활용 1 - 회원 도메인 개발(4) (0) | 2022.06.10 |
스프링부트 JPA활용 1 - 애플리케이션 구현 준비(3) (0) | 2022.06.10 |
스프링부트 JPA활용 1 - 도메인 분석 설계(2) (0) | 2022.06.10 |