비즈니스 로직(도메인 로직)이란?
package jpabook.jpashop.domain;
...
public class Order {
...
//==생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){//orderitem여러개
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;
}
}
🤔
생성 메서드를 따로 만드는 이유?
캡슐화:
Order
객체가 항상 일관된 상태를 유지합니다.불변성 유지:
- 직접적으로 생성자를 호출하여 객체를 생성할 수 없도록 하고, 대신 createOrder
메서드를 사용하게 함으로써 객체 생성의 통제를 강화합니다.
OrderItem... orderItems
: 가변 인자
가변 인자는 메서드가 호출될 때 여러 개의 인자를 배열처럼 받을 수 있게 해준다
- ex) Order.createOrder(member, delivery, orderItem1, orderItem2, orderItem3);
//== 비즈니스 로직 ==//
//주문 취소
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {//배송이 이미 완료(COMP)된 상태
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL); //취소
for (OrderItem orderItem : orderItems) { //for문 돌면서 재고 수량 바꾸기
orderItem.cancle();
}
}
//==조회 로직==//
//전체 주문 가격 조회
public int getTotalPrice() { //내가 주문한 전체 가격
int totalPrice = 0;
for (OrderItem orderItem : orderItems){
totalPrice +=orderItem.getTotalPrice();
}
return totalPrice;
}
}
package jpabook.jpashop.domain;
import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice; // 주문 가격
private int count; // 주문 수량
//==생성 메서드==//
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 cancle() { //재고수량을 원복시킨다
getItem().addStock(count);
}
//==조회 로직==//
//주문 상품 전체 가격 조회
public int getTo
}
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Delivery;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.repository.MemberRepository;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@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.findeOne(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 cancleOrder(Long orderId){
//주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
//주문 취소
order.cancel();
}
}
주문 메서드 - 주문 저장 코드
orderRepository.save(order);
Order
엔티티를 저장할 때Cascade
옵션 덕분에 관련된OrderItem
과Delivery
도 자동으로 저장되므로, 별도로 이들 각각을 저장할 필요가 없다!→ 자동으로 orderitem이랑 delivery도 persist
public class Order {
...
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
...
}
비즈니스 로직 처리 방식?
주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다. 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다
실행 시점에 쿼리의 일부가 변경(사용자 입력 또는 프로그램 로직에 따라, 쿼리의 조건이나 구조를 동적으로 결정해야 하는 경우)될 수 있는 쿼리
동적 SQL요소를 사용해 동적 쿼리를 작성:
<if>
, <choose>
, <where>
, <when>
, <otherwise>
, <foreach>
, <set>
, <trim>
package jpabook.jpashop.repository;
...
@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);
}
//jpql로 처리
public List<Order> findAllByString(OrderSearch orderSearch) {
//language=JPAQL
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); //최대 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<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();
}
}
JPA에서의 Update에 관한 쿼리문은 변경감지(Dirty checking)와 병합(Merge)을 통해 이루어진다
객체만 생성한 상태
Member member = new Member();
영속성 컨텍스트에 의해 관리되어지는 상태
- em.persist()를 통해 영속성 컨텍스트에 저장
- em.find()를 통해 DB에서 엔티티를 조회
트랜잭션 안에서 entity를 조회해야 영속성 상태로 조회된다
em.persist(member)
영속성 컨텍스트에 저장되었다가 분리된 상태
영속 상태X
- em.detach(member); //멤버만 준영속 상태로 전환
- em.clear() : 영속성 컨텍스트를 완전히 초기화
- em.close() : 영속성 컨텍스트를 종료
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한다.
findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
}
entityManager
로 entity
를 직접 꺼내, 값을 수정하는 방식 @Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(itemParam); //병합
}
🚨1) 준영속 엔티티(Member)의 식별자 값으로 1차 캐시 또는 DB에서 영속엔티티(mergeMember)을 조회
2) 조회한 영속엔티티(mergeMember)의 값(회원1)을 준영속 엔티티(Member)의 값(회원명변경)으로 교체 = 병합
3) 트랜잭션 commit 시점에 변경감지를 한 후 Flush 해서 DB에 UPDATE SQL이 실행
엔티티 변경할 때 병합을 사용하면 안되는 이유?
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만(부분 교체),
병합을 사용하면 모든 속성이 변경된다(전체 교체).
→ 병합시 값이 없으면 null
로 업데이트 할 위험도 있다. (모든 필드를 교체해서)