1. "준영속 엔티티"란?
데이터베이스에 식별자가 정확하게 있는 상태의 엔티티를 준영속 상태의 객체로, Persistence Context가 더이상 관리하지 않는 엔티티이다.
Book 객체는 이미 DB에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 Entity도 기존 식별자(Id)를 갖고 있으면 준영속 엔티티로 볼 수 있다.
// getId()로 꺼내온 book
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
...
아래 Order Entity의 cancel 로직에서 OrderStatus를 변경하면 JPA가 관리하기 때문에 "변경 감지"를 통해 자동으로 반영된다. (굳이 orderRepository에 저장하는 로직을 호출할 필요가 없다)
// Order Entity - 주문 취소 로직
public void cancel() {
this.setStatus(OrderStatus.CANCEL);
}
2. 준영속 엔티티 수정 방식
준영속 엔티티의 경우 JPA가 관리하지 않기 때문에, "변경 감지"가 일어나지 않는데, 준영속 엔티티를 수정하는 2가지 방법은 다음과 같다.
① 변경 감지 기능 사용
② 병합(merge) 사용
1) 변경 감지 기능 사용
마지막에 itemRepository의 save를 호출하지 않아도 된다. 왜그럴까?
영속 상태의 엔티티의 수정 로직이 마치면 Spring의 @Transacactional에 의해서 트랜잭션이 commit이 된다. 그러면 JPA는 영속성 컨텍스트에 있는 Entity 중에 변경 된 것을 다 찾아서 반영하는 flush를 실행하여 DB에 처리한다.
// ItemService.java
@Transactional
public void updateItem(Long itemId, Book bookParam) {
// DB에 저장된 영속성 엔티티를 찾아온다
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(bookParam.getPrice());
findItem.setName(bookParam.getName());
findItem.setStockQuantity(bookParam.getStockQuantity());
// itemRepository.save(findItem); -> 호출할 필요가 없다
}
2) 병합 사용(Merge)
Merge는 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다. 다시 말하면, item의 모든 데이터를 바꿔치는 것이다. 결국 트랜잭션이 commit 될 때 반영된다.
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
동작 방식은 다음과 같다.
① 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
② 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다.
③ 트랜잭션 Commit 시점에 변경 감지 기능이 동작하여 데이터베이스에 UPDATE SQL이 실행된다.
하지만 Merge는 모든 속성(필드)이 변경되기 때문에 병합 시 값이 없으면 null로 업데이트될 위험이 있다. 어떤 필드 값을 선택할 수 있는 개념이 아니다.
3. 최종 수정 방식
그러므로 Merge를 사용하지말고, 변경할 필드들만 변경하도록 하는 것을 권장한다. 이때 set을 남발하는 것이 아닌 change 등 의미있는 메서드를 선언하고 이를 사용해야 변경 지점을 Entity 레벨에서 확인할 수 있다.
@Transactional
public void updateItem(Long itemId, Book bookParam) {
// DB에 저장된 영속성 엔티티를 찾아온다
Item findItem = itemRepository.findOne(itemId);
// O
findItem.change(price, name, stockQuantity);
// X
findItem.setPrice(bookParam.getPrice());
findItem.setName(bookParam.getName());
findItem.setStockQuantity(bookParam.getStockQuantity());
// itemRepository.save(findItem); -> 호출할 필요가 없다
}
또한 필요한 데이터만 전달하기 위해 컨트롤러에서 어설프게 Entity를 생성하는 것을 지양한다. (DTO를 사용하는 방법도 있다)
@PostMapping("/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @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);
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemId); // 영속 상태의 Entity 조회
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
그런데 앞서 말했듯이 Entity 레벨에서 수정하는 메서드를 작성해서 호출하는 것이 유지보수에 더 좋기 때문에 다음과 같이 수정한다.
// itemService
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemId); // 영속 상태의 조회
findItem.changeItem(name, price, stockQuantity);
// findItem.setName(name);
// findItem.setPrice(price);
// findItem.setStockQuantity(stockQuantity);
}
// Item Entity
public void changeItem(String name, int price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
'🌱 Spring > JPA ①' 카테고리의 다른 글
[Spring] Member/Item 등록, 조회(필수 입력 필드 처리) (0) | 2023.08.23 |
---|---|
[Spring] 주문 Entity, Repository, Service 개발 (0) | 2023.08.09 |
[Spring] 상품 Entity, Repository, Service 개발 (0) | 2023.08.05 |
[Spring] 회원 Service Unit Test 작성하기 (0) | 2023.08.05 |
[Spring] 회원 Service & Repository 개발 (0) | 2023.08.04 |