5주차
1. 상품 엔티티 개발(비즈니스 로직 추가)
//==비즈니스 로직==//
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
재고를 추가 혹은 감소시키는 비즈니스 로직 추가.
removestock()은 재고가 부족하면 예외를 발생시킨다.
2. 상품 리포지토리 개발
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
save()는 id가 없으면 신규로 보고 persist(새로 저장)
id가 있으면 merge(update)
3. 상품 서비스 개발
서비스는 리포지토에 단순히 위임만 하는 클래스이다.
크게 추가되는 기능이 없음.
6주차
1. 주문, 주문상품 엔티티 개발
//==생성 메서드==//
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;
}
}
Order 기능들
- createOrder()(생성 메서드) : 주문 엔티티를 생성. 주문 회원, 배송정보, 주무상품의 정보를 받는다.
- cancel()(주문 취소) : 주문 취소. 주문 상태를 취소로 변경하고 주문 상품에 주문 취소를 알린다. 이미 배송을 완료한 상품이면 주문 취소가 불가능하도록 예외 발생
- getTotalPrice()(전체 주문 가격 조회) : 각 주문 상품 가격 조회 후 더함
//==생성 메서드==//
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();
}
OrderItem 기능들
- createOrderItem()(생성 메서드) : 주문 상품, 가격, 수량 정보를 사용해서 주문 상품 엔티티 생성. item.removeStock(count)로 주문한 수량만큼 재고를 줄인다.
- cancel()(주문 취소) : getItem().addStock(count)로 취소한 수량만큼 상품 재고 증가
- getTotalPrice()(주문 가격 조회) : 주문 가격 * 수량
2. 주문 리포지토리 개발
검색은 동적 쿼리를 이용한다(5에서 계속).
3. 주문 서비스 개발
@Teansactional(readOnly = True) 이므로 data를 변경하는 메서드는 @Transactional을 입력해준다.
@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();
}
/** 주문 검색 */
/*
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch);
}
*/
}
주문 서비스 기능들
- order()(주문) : 회원 ID, 상품 ID, 주문 수량 정보를 받아서 실제 주문 엔티티 생성 후 저장
- cancelOrder()(주문 취소) : 주문 ID를 받아서 주문 엔티티를 조회한 후 주문 엔티티에 주문 취고 요청
- findOrders()(주문 검색) : OrderSearch라는 검색 조건을 가진 객체로 주문 엔티티 검색(5 참고)
비즈니스 로직의 대부분이 엔티티에 있고, 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할
이와 같이 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라고 한다.
http://martinfowler.com/eaaCatalog/domainModel.html
반대의 경우는 트랜잭션 스크립트 패턴이라 한다.
http://martinfowler.com/eaaCatalog/ transactionScript.html
4. 주문 기능 테스트
5. 주문 검색 기능 개발
검색 기능은 JPA에서 동적 쿼리를 해결하는 문제
// OrderSearch
@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("재고 수량 부족 예외가 발생해야 한다.");
}
주문 리포지토리 코드의 findAll(OrderSearch orderSearch)메서드는 검색 조건에 동적으로 쿼리를 생성 후 주문 엔티티를 조회한다.
동적 쿼리 처리는 여러 방식이 있다.
- JPQL : 문자로 생성하기는 번거롭고, 실수로 인한 버그가 충분히 발생할 수 있다.
- JPA Criteria : JPA 표준 스펙이지민 실무에서 사용하기 너무 복잡하다(유지보수가 어려움).
- Querydsl : 위 문제들에 대한 대안책
현재는 JPA Criteria로 처리
// OrderRepository
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Order;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
@Repository
public class OrderRepository {
private final EntityManager em;
public OrderRepository(EntityManager em) {
this.em = 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) {
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);
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<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();
}
}
'spring boot' 카테고리의 다른 글
실전! 스프링 부트와 JPA 활용2 - 1주차 (0) | 2023.07.03 |
---|---|
실전! 스프링 부트와 JPA 활용1 - 7주차 (0) | 2023.06.29 |
실전! 스프링 부트와 JPA 활용1 - 3,4주차 (0) | 2023.06.26 |
실전! 스프링 부트와 JPA 활용1 - 2주차 (0) | 2023.06.24 |
실전! 스프링 부트와 JPA 활용1 - 1주차 (0) | 2023.06.22 |