실전 활용1 JPA _개발(주문)
1. 주문,주문상품 엔티티
*주문 도메인의 구현기능
1) 상품 주문 2) 주문 내역 조회 3) 주문 취소
👀연관관계 편의 메서드
✔ 어디에?
답) 보통 비즈니스 로직에서 중심이 되는 엔티티에서 사용하는게 좋음
⇨ Order엔티티와 Member 엔티티 사이의 관계로 주문이 중심이 되기 때문에 Order엔티티에 써준다.
✔ 왜?
답) 양방향이므로 양쪽 모두에 값을 다 넣어주기 위해서
추가) setXXX()메서드와 addXXX()메서드
서비스 로직을 작성할때 더 이해하기 쉬운 것을 둘 중에서 하나 골라쓴다
ex)
(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()
를 사용할 수 있었다.
👏 이에 관하여 질문을 드리고 답을 받았습니다!
※주문 엔티티
1) 주문 생성 메서드
✔createOrder()
메서드
- order 객체 생성하여 회원, 배송지, 주문상품목록에 상품을 연관관계로 값을 싹 세팅 해준다.
- 주문 상태는 '주문'으로 처음 상태로 강제해놓음
- 현재시간으로 맞춰 놓아줌 (
LocalDateTime.now()
)
⇨ 앞으로 생성 메서드를 수정할때, 이 곳에서만 되기 때문에 생성메서드를 만들어 주는게 좋음.
📣OrderItem... orderItems
는 자바 가변인자.
2) 비즈니스 로직
_1. 주문 취소 메서드
-
DeliveryStatus
클래스가 COMP이면 배송완료이므로 예외처리
-배송완료가 아닌 상태라면, 현재 주문상태를 CANCEL로 바꿔줌
⇨주문상태 취소
-주문자가 시킨 다른 주문상품에도 각각 주문취소를 알림
3) 조회 로직
_1. 전체 주문가격 조회
※주문상품 엔티티
1) 주문 생성 메서드
-생성이 단순하지 않으므로 메서드로 따로 정의해준다.
- 왜 Item 엔티티에도
price
가 있는데 여기서도 매개변수로OrderPrice
를 데려오는가?
=> 할인이나 쿠폰 적용의 경우가 있을 수 있기 때문.
2) 비즈니스 로직
_1) cancel() 메서드
Item 엔티티의 addStock()
메서드를 호출해 count
만큼 취소하였으므로 '재고수량'을 증가 시킨다.
3) 조회 로직
_1) getTotalPrice() 금액조회 메서드
orderItem엔티티에서 order엔티티의 호출을 받아 해당 주문상품의 갯수와 가격을 계산하여 값을 반환해준다.
2. 주문 리포지토리 개발
- 스프링 빈 주입 @Repository
@RequireArgsConstruct
'final' 멤버필드가 있는 것만 생성자 생성(생략)- 검색기능은 동적쿼리이므로 마지막에
3. 주문 서비스 개발
📣주의! 예제를 단순화 하기 위해서 주문서비스에서 이렇게 하나의 상품만 받도록 설계했습니다^^
*서비스에서 구현할 기능)
- 주문 2. 주문취소 3. 주문검색
1) cascade와 save()
👏 cascade가 왜쓰였는가?
1.cascade가 쓰일 수 있는 조건은 1)동일한 라이프 사이클, 2) 참조하는 주인이 private owner 일 때 라고 2가지 조건을 충족할 때 쓴다. 그러나 가장 중요한 점은 지금도, 미래에도 다른 곳에서 참조할 가능성이 없어야 한단 것이다.
.2. 양방형, 연관관계 주인등과 상관이 없다. 3. 변경감지와 cascade의 쓰임의 차이가 존재하며 이 곳에서 참조하였습니다.
👏 cascade가 어디에 있는가?
해당 Order엔티티에 orderItem에 찾아가 보면
cascade = CacadeType.ALL
이 있다.
=> 곧 cascade가 있는 곳을 다persist()
를 날려주게 된다. => orderItem, delivery가persist
되었다
*(결론) :
1.cascade는 저장, 삭제의 효과가 다른 엔티티에게 전파되는 것입니다.
2.이해가 안가면 나중에 이런 상황이 보이면 그때 쓰자^^
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에 쿼리를 날린다.
📣이해가 안가면 한 번 더 읽어보자
3) 추가 @NoArgsConstructor(access = AccessLevel.PROTECTED)
유지보수가 힘들게 객체를 만들고 set으로 하나하나씩 하는 사람이 있다. 이를 보고 복잡하게 만들지 않기 위해 쓰인다.
@NoArgsConstructor(access = AccessLevel.PROTECTED) 를 써주며 해당 메서드를 Service에서 직접 구현할 필요 없이 Entity에서 생성메소드와 같이 가져다 쓰라는 뜻이다.
4. 주문 기능 테스트
👀테스트 요구사항
- 품 주문이 성공해야 한다.
- 상품을 주문할 때 재고 수량을 초과하면 안 된다.
- 주문 취소가 성공해야 한다
👀 OrderServiceTest
@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 라이브러리를 사용해서 처리하면 됩니다.
- 결국 중요한 것은 이러한 핵심 비즈니스 로직을 단위 테스트로 작성하는 것 입니다.
단위 테스트에 관하여 강사님의 생각에 대해 더 자세히 작성되어 있습니다.
5. 주문 검색 기능 개발
📣동적쿼리란?
(1) 실행시에 쿼리문장이 만들어져 실행되는 쿼리문을 말한다. 쿼리문이 변하냐 변하지 않느냐에따라 변하지 않으면 정적쿼리, 변한다면 동적쿼리로 생각하면 된다.
- 대부분 동적쿼리를 사용할때에는 텍스트문장으로 쿼리문을 가지고 있다가 실행할때마다 텍스트 쿼리문장을 바꿔서 실행하는 방식을 사용
1) 동적쿼리의 두 가지 방법
_1. JPQL로 처리
/**
* 검색기능 -> 동적쿼리
* (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
/**
* (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이 있다고 하셨다
Author And Source
이 문제에 관하여(실전 활용1 JPA _개발(주문)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dabeen-jung/실전-활용1-JPA-개발주문저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)