비즈니스 로직은 data를 가지고 있는 Entity에 비즈니스 로직이 있어야 응집력이 좋아진다.
그래서 Item Entity에 재고 수량에 증감에 대한 비즈니스 로직을 작성했다.
// 재고 수량 증가 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; }
@RequiredArgsConstructor
fina이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 룸복 어노테이션
// 주문 저장 // Order 엔티티가 Delivery 엔티티와 OrderItem 엔티티를 cascade하여 Order 엔티티가 persist될 때 Delivery, OrderItem 엔티티도 persist된다. // cascade의 범위에 대해서 사람들이 많이 고민 함. order가 delivery와 order item을 관리하니까 딱 이정도 범위에서만 사용 // delivery와 orderitem은 order엔티티 빼고는 아무도 참조하지 않는다. 다른곳에서도 참조하면 cascade를 막 사용해서는 안된다. orderRepository.save(order);
// 주문 상품 생성 OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
주문 상품을 생성하는 코드를 누군가는 아래의 방식으로 작성할 수 있다.
OrderItem orderItem1 = new OrderItem(); orderItem.setCount();
이러한 방식의 문제는 코드가 점점 커질수록 유지보수 하기 어려워진다는 점이다.
그래서 우리는 이러한 방식의 코드를 막아야한다.
방법 1
protected OrderItem(){}
방법 2
도메인 모델 패턴
트랜잭션 스크립트 패턴
동적 쿼리를 처리하는 방식은 몇가지 방식이 있다.
JPA Citerial로 처리
JPA Citerial는 JPA 표준 스펙이지만 실무에서 사용하기에는 너무 복잡하다.
HomeController
@Slf4j 어노테이션
Logger log = LoggerFactory.getLogger(getClass());
resources/templates/home.html
<head th:replace="fragments/header :: header"> <div th:replace="fragments/bodyHeader :: bodyHeader" /> <div th:replace="fragments/footer :: footer" />
랜더링 될 때 각각 fragments의 header,bodyHeader,footer로 바꿔치기 된다.
<a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
href : a 태그의 href 속성은 링크된 페이지의 URL을 명시합니다.
controller/MemberForm
@NotEmpty(message = "회원 이름은 필수 입니다.") private String name;
@NotEmpty
resources/templates/members/createMemberForm
th:object="${memberForm} // memberForm 객체를 계속 사용하겠다. <input type="text" th:field="*{name}"
*표시가 되어있으면 memberForm 오브젝트를 사용한다는 의미. Getter, Setter로 MemberForm 클래스를 접근한다.
Member 클래스
@Getter @Setter public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
controller/MemberController
@Vaild 어노테이션
@NotNull, @Size, @Pattern 등의 검증 어노테이션을 사용하여 필드의 값이 null이 아니고, 특정 크기를 갖고, 특정 패턴을 따르는지 등을 검증할 수 있다.
public String create(@Valid MemberForm form)
controller/MemberController.
BindingResult
@Valid MemberForm form
public String create(@Valid MemberForm form, BindingResult result)
폼 객체를 써야하는지 엔티티를 집적 사용해야 하는지 문제가 있다.
@PathVariable
@GetMapping("/items/{itemId}/edit") url에서 {itemId} 부분은 동적으로 변하는 아이템 Id를 나타낸다. 이 아이템 Id를 컨트롤러에서 메서드에서 추출하려면 @PathVariable 어노테이션을 사용한다.
public String updateItemForm(@PathVariable("itemId") @PathVariable 어노테이션은 {itemId} 경로 변수의 값을 itemId 매개변수로 추출한다. 추출한 itemId를 이용해 해당 아이템 정보를 조회하고, 적정한 응답을 반환하는 로직을 구현할 수 있다.
@ModelAttribute
준영속 엔티티
영속성 컨텍스트가 더는 관리하지 않는 엔티티
ItemController
public String updateItem( @ModelAttribute("form") BookForm form) {
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/items";
}
book은 새로운 book이 아닌 Id가 세팅이 되어있다. 이 말은 뭔가 JPA의 한 번 들어갔다 나온 애라는 거다. 그래서 이 book은 준영속 상태의 객체가 된다.
준영속 엔티티를 수정하는 2가지 방법
변경 감지 기능 사용
@Transactional
public void updateItem(Long itemId, Book param){ // param: 파라미터로 넘어온 준영속 상태의 엔티티
Item findItem = itemRepository.findOne(itemId); // 같은 엔티티를 조회한다.
// 데이터를 수정한다.
findItem.setPrice(param.getPrice());
findItem.setName(param.getName());
findItem.setStockQuantity(param.getStockQuantity());
}
트랜잭션이 커밋되면 flush가 영속성 컨텍스트에 병견된 값을 보고 DB에 UPDATE쿼리를 날려 값을 수정한다. 이게 변경 감지 기능이다.
Setter를 쓰지 말고 의미있는 메서드를 하나 만들어서 사용, 파라미터가 너무 많으면 DTO를 만들어서 사용
병합(merge)사용
ItemRepository
public void save(Item item){
if(item.getId()== null){
em.persist(item);
} else{
Item merge = em.merge(item);
}
}
em.merge(item) merge를 실행할 때 불러온 item은 영속상태로 변하진 않고, Item merge가 영속성 컨택스트 에서 관리되는 객체이다. 더 수정할 일이 있으면 merge객체를 사용해야 한다.
병합 동작 방식의 문제점
그니까 가급적이면 merge를 쓰지 말고 변경 감지를 써라.