Order
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
...
//==생성 메서드==//
//order가 연관관계를 걸면서 세팅이 되고 상태랑 주무시간까지 세팅하면서 생서해줌
public static Order createOrder
(Member member/* -> 주문하는 사람*/, Delivery delivery/* -> 배송 정보*/, OrderItem... orderItems/* -> 여러개 주문*/) {
Order order = new Order(); //Order 생성
order.setMember(member); //주문자 set
order.setDelivery(delivery); //배송정보 set
for (OrderItem orderItem : orderItems) { //현재 생성한 order에다가
order.addOrderItem(orderItem); // item 추가
}
order.setStatus(OrderStatus.ORDER); //order 상태를 처음 상태로 강제로 넣음
order.setOrderDate(LocalDateTime.now()); //주문 시간을 현재 시간으로 넣어줌
return order;
}
//==비즈니스 로직==//
/** 주문 취소 */
public void cancel() {
//배송 완료가 되어버리면
if (delivery.getStatus() == DeliveryStatus.COMP) {
//예외처리
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
//order의 상태를 cancel로 변경
this.setStatus(OrderStatus.CANCEL);
//주문한 상품들에게 취소를 각각 날려줌
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
//==조회 로직==//
/** 전체 주문 가격 조회 */
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
}
OrderItem
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import jpabook.jpashop.domain.item.Item;
import javax.persistence.*;
@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {
...
//==생성 메서드==//
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();
//다음 방식으로 변경 가능->
return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
}
}
OrderRepository
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) { ... }
}
OrderService
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final MemberRepository memberRepository;
private final OrderRepository orderRepository;
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());
delivery.setStatus(DeliveryStatus.READY);
//주문상품 생성
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();
}
/** 주문 검색 */
//OrderSearch라는 검색 조건을 가진 객체로 주문 엔티티를 검색
/*
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch);
}
*/
}
❗참고❗
주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다. 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다. 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라 한다. 반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.
테스트 요구사항
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@PersistenceContext
EntityManager em;
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception {
/*
//given
Book book = new Book();
book.setName("시골 JPA");
book.setPrice(10000);
book.setStockQauntity(10);
em.persist(book);
int orderCount = 2;
//when
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
*/
//Given : 테스트를 위한 회원과 상품을 만들고
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고
int orderCount = 2;
//When : 실제 상품을 주문하고
Long orderId = orderService.order(member.getId(), item.getId(), orderCount);
//Then : 주문 가격이 올바른지, 주문 후 재고 수량이 정확히 줄었는지 검증
Order getOrder = orderRepository.findOne(orderId);
assertEquals("상품 주문시 상태는 ORDER",OrderStatus.ORDER, getOrder.getStatus());
assertEquals("주문한 상품 종류 수가 정확해야 한다.",1, getOrder.getOrderItems().size());
assertEquals("주문 가격은 가격 * 수량이다.", 10000 * 2, getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야 한다.",8, item.getStockQuantity());
}
// 재고 수량을 초과해서 상품을 주문했을 때 `NotEncoughStockException` 예외가 발생
@Test(expected = NotEnoughStockException.class)
public void 상품주문_재고수량초과() throws Exception {
//Given
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고
int orderCount = 11; //재고보다 많은 수량
//When
orderService.order(member.getId(), item.getId(), orderCount);
//Then
fail("재고 수량 부족 예외가 발생해야 한다.");
}
@Test
public void 주문취소() {
//Given : 주문
Member member = createMember();
Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고
int orderCount = 2;
Long orderId = orderService.order(member.getId(), item.getId(),orderCount);
//When : 해당 주문을 취소
orderService.cancelOrder(orderId);
//Then :: 주문상태가 주문 취소 상태인지, 취소한 만큼 재고가 증가했는지 검증
Order getOrder = orderRepository.findOne(orderId);
assertEquals("주문 취소시 상태는 CANCEL 이다.",OrderStatus.CANCEL, getOrder.getStatus());
assertEquals("주문이 취소된 상품은 그만큼 재고가 증가해야 한다.", 10, item.getStockQuantity());
}
private Member createMember() {
Member member = new Member();
member.setName("회원1");
member.setAddress(new Address("서울", "강가", "123-123"));
em.persist(member);
return member;
}
private Book createBook(String name, int price, int stockQuantity) {
Book book = new Book();
book.setName(name);
book.setStockQuantity(stockQuantity);
book.setPrice(price);
em.persist(book);
return book;
}
}
OrderSearch
package jpabook.jpashop.domain;
@Getter @Setter
public class OrderSearch {
private String memberName; //회원 이름
private OrderStatus orderStatus;//주문 상태[ORDER, CANCEL]
}
OrderRepository : JPQL로 처리
//JPQL 쿼리를 문자로 생성하기는 번거롭고, 실수로 인한 버그가 충분히 발생할 수 있다.
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> findAllByString(OrderSearch orderSearch) {
//language=JPAQL
//setMaxResults(100) : 결과를 최대 100개 보여줌
//Order 조회후 member와 조인
String jpql = "select o From Order o join o.member m";
boolean isFirstCondition = true;
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) { //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();
}
}
OrderRepository : JPA Criteria로 처리
//JPA Criteria는 JPA 표준 스펙이지만 실무에서 사용하기에 너무 복잡하다.
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();
}
OrderRepository : Querydsl로 처리
//Java 코드이기 때문에 오타가 나도 컴파일 시점에서 잡을 수 있다.
public List<Order> findAll(OrderSearch orderSearch) {
QOrder order = Qorder.order;
QMember member = QMember.member;
return query
.select(order)
.from(order)
.join(order.member, member)
.where(statusEq(orderSearch.getOrderStatus()),
nameLike(orderSearch.getMemberName()))
.limit(1000)
.fetch();
}
private BooleanExpression statusEq(OrderStatus statusCond) {
if (statusCond == null) {
return null;
}
return order.status.eq(statusCond;)
}
private BooleanExpression nameLike(String nameCond) {
if (!StringUtils.hasText(nameCond)) {
return null;
}
return member.name.like(nameCond);
}