1) 상품 주문 2) 주문 내역 조회 3) 주문 취소
✔ 어디에?
답) 보통 비즈니스 로직에서 중심이 되는 엔티티에서 사용하는게 좋음
⇨ Order엔티티와 Member 엔티티 사이의 관계로 주문이 중심이 되기 때문에 Order엔티티에 써준다.
✔ 왜?
답) 양방향이므로 양쪽 모두에 값을 다 넣어주기 위해서
서비스 로직을 작성할때 더 이해하기 쉬운 것을 둘 중에서 하나 골라쓴다
(1)
//Delivery와 Order 관계
//delivery.getOrder()시 ->delivery 엔티티에서 order 객체를 반환함
public void setDelivery(Delivery delivery) {
this.delivery = delivery; //배송지 설정
//Order 객체를 반환 => add()를 넣지 x
delivery.setOrder(this);
}
delivery.getOrder(this)
을 했을때 반환이 객체가 나오게 된다면 컬렉션의 add()를 쓸 수 없으므로 ,delivery.setOrder(this)
와 같이set()
으로 써줘야 할 수도 있다.
(2)
//Member와 Order 엔티티
public void setMember(Member member) {
this.member = member;
//Member엔티티에선 List<Order> orders 리스트 반환 => add()를 사용o
member.getOrders().add(this);
}
- 반면에 이
setMember()
메서드는 같은 set()메서드지만member.getORders()
반환시 해당 orders 리스트가 반환되므로 -> 컬렉션add()
를 사용할 수 있었다.
👏 이에 관하여 질문을 드리고 답을 받았습니다!
✔createOrder()
메서드
- order 객체 생성하여 회원, 배송지, 주문상품목록에 상품을 연관관계로 값을 싹 세팅 해준다.
- 주문 상태는 '주문'으로 처음 상태로 강제해놓음
- 현재시간으로 맞춰 놓아줌 (
LocalDateTime.now()
)
⇨ 앞으로 생성 메서드를 수정할때, 이 곳에서만 되기 때문에 생성메서드를 만들어 주는게 좋음.
📣OrderItem... orderItems
는 자바 가변인자.
-
DeliveryStatus
클래스가 COMP이면 배송완료이므로 예외처리
-배송완료가 아닌 상태라면, 현재 주문상태를 CANCEL로 바꿔줌
⇨주문상태 취소
-주문자가 시킨 다른 주문상품에도 각각 주문취소를 알림
-생성이 단순하지 않으므로 메서드로 따로 정의해준다.
- 왜 Item 엔티티에도
price
가 있는데 여기서도 매개변수로OrderPrice
를 데려오는가?
=> 할인이나 쿠폰 적용의 경우가 있을 수 있기 때문.
Item 엔티티의
addStock()
메서드를 호출해 count
만큼 취소하였으므로 '재고수량'을 증가 시킨다.
orderItem엔티티에서 order엔티티의 호출을 받아 해당 주문상품의 갯수와 가격을 계산하여 값을 반환해준다.
- 스프링 빈 주입 @Repository
@RequireArgsConstruct
'final' 멤버필드가 있는 것만 생성자 생성(생략)- 검색기능은 동적쿼리이므로 마지막에
📣주의! 예제를 단순화 하기 위해서 주문서비스에서 이렇게 하나의 상품만 받도록 설계했습니다^^
1.cascade가 쓰일 수 있는 조건은 1)동일한 라이프 사이클, 2) 참조하는 주인이 private owner 일 때 라고 2가지 조건을 충족할 때 쓴다. 그러나 가장 중요한 점은 지금도, 미래에도 다른 곳에서 참조할 가능성이 없어야 한단 것이다.
.2. 양방형, 연관관계 주인등과 상관이 없다. 3. 변경감지와 cascade의 쓰임의 차이가 존재하며 이 곳에서 참조하였습니다.
해당 Order엔티티에 orderItem에 찾아가 보면
cascade = CacadeType.ALL
이 있다.
=> 곧 cascade가 있는 곳을 다persist()
를 날려주게 된다. => orderItem, delivery가persist
되었다
*(결론) :
1.cascade는 저장, 삭제의 효과가 다른 엔티티에게 전파되는 것입니다.
2.이해가 안가면 나중에 이런 상황이 보이면 그때 쓰자^^
/** 주문 취소 */
//주문 아이디만 알면 되네
@Transactional
public void cancelOrder(Long orderId) {
//주문 id로 해당 주문엔티티 조회
Order order = (Order) orderRepository.findOne(orderId);
//주문 취소 메서드
//*변경감지 (jpa가 데이터가 변경된 것을 감지하여 DB에 쿼리를 날려줌)
//->cancel()메서드 내 orderStatus()변경으로 자동으로 감지, addStock()변경 자동감지
order.cancel();
}
cancel()메서드 내 orderStatus()변경으로 자동으로 감지, addStock()변경 자동감지
=> 영속상태에서 데이터가 변경된 것을 감지하여 DB에 쿼리를 날린다.
📣이해가 안가면 한 번 더 읽어보자
유지보수가 힘들게 객체를 만들고 set으로 하나하나씩 하는 사람이 있다. 이를 보고 복잡하게 만들지 않기 위해 쓰인다.
@NoArgsConstructor(access = AccessLevel.PROTECTED) 를 써주며 해당 메서드를 Service에서 직접 구현할 필요 없이 Entity에서 생성메소드와 같이 가져다 쓰라는 뜻이다.
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
@PersistenceContext EntityManager em;
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
public void 상품주문() throws Exception {
//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("메세지", 기댓값, 실제값)
assertEquals("상품 주문시 상태는 ORDER",OrderStatus.ORDER,
getOrder.getStatus());
assertEquals("주문한 상품 종류 수가 정확해야 한다.",1,
getOrder.getOrderItems().size());
assertEquals("주문 가격은 가격 * 수량이다.", 10000 * 2,
getOrder.getTotalPrice());
assertEquals("주문 수량만큼 재고가 줄어야 한다.",8, item.getStockQuantity());
}
@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);//해당 주문id
assertEquals("주문 취소시 상태는 CANCEL 이다.",OrderStatus.CANCEL,
getOrder.getStatus());
assertEquals("주문이 취소된 상품은 그만큼 재고가 증가해야 한다.", 10,
item.getStockQuantity()); //주문이 취소되었으므로 재고 자체가 10개로 원래대로 복구
}
//NotEnoughStock... ->이 예외가 터져야 함
@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("재고 수량 부족 예외가 발생해야 한다.");
}
//==given에 쓰이던 것들을 다른 테스트에서도 쓰여야 하니까 걍 따로 메서드로 생성==//
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;
}
- DB와 상관없이 단위 테스트를 하는 것이 중요합니다.
- 핵심은 스프링 컨테이너나 특정 DB같은 하부 인프라 구조에 의존하지 않고, 핵심 비즈니스 로직(서비스, 엔티티)를 테스트 할 수 있으면 됩니다.
- 이때 Repository는 Mock 라이브러리를 사용해서 처리하면 됩니다.
- 결국 중요한 것은 이러한 핵심 비즈니스 로직을 단위 테스트로 작성하는 것 입니다.
단위 테스트에 관하여 강사님의 생각에 대해 더 자세히 작성되어 있습니다.
(1) 실행시에 쿼리문장이 만들어져 실행되는 쿼리문을 말한다. 쿼리문이 변하냐 변하지 않느냐에따라 변하지 않으면 정적쿼리, 변한다면 동적쿼리로 생각하면 된다.
/**
* 검색기능 -> 동적쿼리
* (1) JPQL 방법
*JPA Criteria(JpaSpecificationExecutor 포함)을 사용하지 마시고,
* 단순해도 다른 방법으로 푸시는 것을 권장합니다.
* !!! 현재 가장 좋은 방법은 Querydsl이라는 기술을 사용하는 것입니다.
* 자바 코드로 쿼리를 작성해서 컴파일 시점에 오류를 잡아주고,
* 자바 코드를 활용해서 매우 깔끔하게 동적 쿼리를 작성할 수 있습니다.
*/
public List<Order> findAllByString(OrderSearch orderSearch) {
//jpql을 동적으로 만들기 위해
//(1)JPQL 문자로 만드는 방법 -> 지옥의 방법
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";
}
//회원 이름 검색
//hasText() 값이 잇다면
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) {//orderStatus가 있다면
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if (StringUtils.hasText(orderSearch.getMemberName())) {//Member가 있다면
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
코드만 봐도 길고 지친다... 너무 내용이 복잡하다.
/**
* (2) JPA Criteria -> 이것도 어려움;
* jpql을 자바코드로 작성하게 해주게 '표준 방법임'
단점: 유지보수가 안좋음-> 직관적이지 x
*/
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();
}
(1)JPA Criteria 또한 너무 길고 가독성이 좋지 않으며, jpa 표준인 (2)JPA Criteria 조차 코드가 너무 복잡하고 직관적이지 않아 이해가 어렵다.
'동적쿼리'에 대해 많은 개발자 분들이 고민을 하시고 계시고 현재 가장 멋진 해결방법은 Querydsl이 있다고 하셨다