//--생성 메서드==//
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 totalPrice;
}
domain/OrderItem.java
cancel 메서드 추가 (비즈니스 로직) : 취소한 수량만큼 상품의 재고를 증가한다.
생성 메서드 createOrderItem() : 각 정보를 사용해서 주문 상품 엔티티 생성, item.removeStock() 호출해서 주문한 수량만큼 상품의 재고는 차감한다.
주문 가격 조회 : 주문가격 * 수량 값을 반환
//==생성 메서드==//
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();
}
주문 리포지토리 개발
repository/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){}
}
엔티티를 저장하고 검색
주문 서비스 개발
service/OrderService.java
주문을 데이터로 변환하므로 @Transactional 필요
order가 persist 될 때 castcade 걸린것 다 실행해서 orderItem과 delivery가 다 실행된다.
protected 통해서 생성자 다른 형태로 만드는 것 막을 수 있다. lombok으로도 막을 수 있음.
@NoArgsConstructor(access=AccessLevel.PROTECTED)
코드를 제약하는 스타일로 짜는 것이 설계, 유지 보수에 좋다!!!
JPA의 장점 : 변경한 데이터를 바깥에서 업데이트 쿼리를 짜서 날릴 필요가 없다.
@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();
}
}
★ 주문 서비스의 주문과 취소 메서드는 비즈니스 로직이 대부분 엔티티에 있다.
서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할만 수행한다. 이런 스타일을 '도메인 모델 패턴'이라고 한다. JPA나 ORM 쓰면 이런식으로 도메인 안에 로직을 다 넣는다.
반대로 엔티티에 로직이 별로 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 '트랜잭션 스크립트 패턴'이라고 한다.
어떤 쪽이 더 유지 보수하기 쉬운지 고민해서 채택해서 사용하도록 하자~
한 트랜잭션 안에서도 양립 가능!
주문 기능 테스트
상품 주문 성공
상품 주문 시 재고수량을 초과하면 안된다
주문 취소가 성공해야함
위의 조건들을 만족하는지 테스트해보자.
OrderServiceTest.java
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.exceptioin.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 static org.aspectj.bridge.MessageUtil.fail;
import static org.junit.Assert.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@Autowired EntityManager em;
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception {
//given
Member member = createMember();
Book book = createBook("시골 JPA", 10000, 10);
int orderCount = 2;
//when
Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
//then
Order getOrder = orderRepository.findOne(orderId);
assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
assertEquals("주문 가격은 가격 * 수량이다.", 10000 * orderCount, getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야 한다.", 8, book.getStockQuantity());
}
@Test
public void 주문취소() throws Exception {
//given
Member member = createMember();
Book 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());
}
@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("재고 수량 부족 예외가 발생해야 한다.");
}
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("회원1");
member.setAddress(new Address("서울", "강가", "123-123"));
em.persist(member);
return member;
}
}
Assert는 인수를 검증하고 조건에 맞지 않는지 확인
→ assertEquals, fail 등
주문 초과하는 수량으로 테스트해보면 로직에서 예외가 발생.
order entity에 대해서 비즈니스 로직을 작성해버리는 것도 가능.
단위 테스트로 작성하는 것이 중요.
주문 검색 기능 개발
JPA에서 동적 쿼리를 어떻게 해결해야 하는가?
repository/OrderSearch.java
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]
}
repository/OrderRepository.java
동적 쿼리 생성
findAll 함수 작성시 세가지 방법
(1) JPQL로 처리
(생략..) 안씀
(2) JPA Criteria로 처리
/**
* 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<Object, Object> 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);
return query.getResultList();
}