
김영한 강사님 [실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 강의 참조
생성 메서드( createOrder() ): 주문 회원, 배송정보, 주문상품의 정보를 받아서 실제 주문 엔티티를 생성
주문 취소( cancel() ): 주문 상태를 취소로 변경하고 주문상품에 주문 취소를 알린다. 만약 이미 배송을 완료한 상품이면 주문을 취소하지 못하도록 예외를 발생시킨다.
전체 주문 가격 조회: 주문 시 사용한 전체 주문 가격을 조회. 연관된 주문상품들의 가격을 조회해서 더한 값을 반환
// == 생성 메서드 (주문생성) == //
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
order.setOrderDate(LocalDateTime.now());//주문시간 : 현재시간
return order;
}
// == 비즈니스 로직 == //
//주문 취소
public void cancel(){
if(delivery.getStatus()==DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);//주문상태 : CANCEL
for (OrderItem orderItem: orderItems) {
orderItem.cancel();//재고수량 증가
}
}
// == 조회 로직 == //
//전체 주문 가격 조회
public int getTotalPrice(){
int totalPrice = 0;
for (OrderItem orderItem: orderItems) {
totalPrice += orderItem.getTotalPrice();
//주문가격과 주문수량이 필요하므로 orderitem메소드 호출
}
return totalPrice;
}
}
생성 메서드( createOrderItem() ): 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성. 주문한 수량만큼 상품의 재고를 줄인다.
주문 취소( cancel() ): 취소한 주문 수량만큼 상품의 재고를 증가시킨다.
주문 가격 조회( getTotalPrice() ): 주문 가격에 수량을 곱한 값을 반환
// == 생성 메서드 (주문아이템 생성) == //
public static OrderItem createOrderItem(Item item, int orderPrice, int count){
//쿠폰받거나 할인받을 수 있기 때문에 orderprice 따로 작성
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();
}
}
public static Order createOrder(OrderItem... orderItems){}
- 위 코드에서 ...은 가변 인자(variable arguments)를 의미 (메소드에서 인자의 개수가 정해져 있지 않은 경우에 사용)
- OrderItem... orderItems는 OrderItem 객체가 0개 이상인 배열을 가변 인자로 받는 것을 나타낸다. 이렇게 선언된 가변 인자는 메소드에서 배열 형태로 처리.
- 예를 들어, createOrder 메소드를 호출할 때 OrderItem 객체를 3개 전달하고 싶다면
OrderItem orderItem1 = new OrderItem(); OrderItem orderItem2 = new OrderItem(); OrderItem orderItem3 = new OrderItem(); Order.createOrder(orderItem1, orderItem2, orderItem3);
- 이렇게 하면 createOrder 메소드에서는 OrderItem 객체를 담은 배열이 아니라, 개별적인 인자로 받아서 배열로 묶어서 처리(OrderItem... orderItems는 가변 인자를 배열 형태로 처리하는 것을 의미)
@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);
}
}
//주문
@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();
}
- 엔티티 클래스에서 연관관계 매핑 시 cascade = CascadeType.ALL : 상위 엔티티에서 하위 엔터티로 모든 작업을 전파
- order 클래스만 persist 해주면 cascade = CascadeType.ALL로 매핑된 orderitem과 delivery도 자동 persist됨
- cascade 설정 범위는 참조하는 대상이 private owner일 경우에 사용 (orderitem과 delivery는 order만 참조함)
- 참조하는 대상이 많을 경우에는 repository를 따로 생성해서 작성하는 것을 권장
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
- new연산자를 통해서 객체를 직접 생성하는 것 권장 하지 않음.
- 생성 메소드를 통하지 않고 객체를 생성할 경우 유지보수의 어려움이 있으므로 생성하지 못하게 설정
protected OrderItem() {}: JPA는 protected까지 허용@NoArgsConstructor(access = AccessLevel.PROTECTED)
- 1번과 2번 같은 의미
JPA 활용 시 엔티티 안에있는 데이터만 변경하면 jpa가 알아서 바뀐 변경 포인트들을 변경내역감지(dirty checking)를 통해 변경된 내역 찾아서 db에 업데이트 쿼리가 자동으로 전달
도메인 모델 패턴
트랜잭션 스크립트 패턴
테스트 요구사항
예상값과 실제값 불일치시 test fail

package jpabook.jpashop.service;
@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);
Assert.assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
Assert.assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
Assert.assertEquals("주문 가격은 가격 * 수량이다.", 10000 * orderCount, getOrder.getTotalPrice() );
Assert.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);
Assert.assertEquals("주문 취소시 상태는 CANCEL 이다.", OrderStatus.CANCEL, getOrder.getStatus());
Assert.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
Assert.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("홍길동");
member.setAddress(new Address("서울", "강남구", "12345"));
em.persist(member);
return member;
}
}
- 내부/외부조인 (둘 이상의 테이블에 존재하는 공통 속성의 값이 같은 것을 결과로 추출)
SELECT m FROM Member m [INNER/OUTER] JOIN m.team t WHERE t.name = :teamName- 회원(별칭:m)을 검색하고 회원이 가지고 있는 연관 필드로 팀(별칭:t)과 조인한다.(조건 : team이름이 parameter값)
- 세타 조인 : 동등조인이면서 동시에 sql에서 join구문 없이 사용하는 것 (서로 연관관계가 없을 때 사용)
- 동등 조인(equi join) : 양쪽 테이블에서 조인 조건이 일치하는 행
select count(m) from Member m, Team t where m.username = t.name
- ON 절 (조인 대상 필터링)
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'- 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
- ON 절 (연관관계 없는 엔티티 외부 조인)
SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name- 회원의 이름과 팀의 이름이 같은 대상 외부 조인
//검색결과 조회
public List<Order> findAll(OrderSearch orderSearch){
return em.createQuery("select o from Order o join o.member m " +
"where o.status =:status and m.name like :name", Order.class)
.setParameter("status", orderSearch.getOrderStatus())//파라미터 바인딩
.setParameter("name", orderSearch.getMemberName())
.setMaxResults(1000)//최대 1000건까지 조회
.getResultList();
}
//JPA criteria를 이용한 동적 쿼리(권장하지 않음 Querydsl을 권장)
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();
}
정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크
장점
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();
}