🧩 아이템 수정 - 변경감지와 머지(merge)
준영속(detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태로 현재는 영속상태가 아닌 상태이다.
- 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.
준영속 엔티티를 수정하는 2가지 방법
- 변경 감지 기능 사용 ( 권장 )
- 병합(merge)사용 (권장 X)
* 준영속과 비영속의 차이점
- 준영속과 비영속의 차이는 바로 영속상태가 되어본 경험이다.
- 영속상태가 되기 위해서는 식별자 반드시 필요하다.
- 준영속 상태의 엔티티는 식별자가 존재한다.
- 비영속 상태의 엔티티는 식별자가 존재할수도, 존재하지 않을 수도 있다.
@PostMapping("/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") Item form) {
Item item = new Item();
item.setId(item.getId());
item.setName(item.getName());
item.setPrice(item.getPrice());
item.setStockQuantity(item.getStockQuantity());
itemService.updateItem(itemId,form.getName(),form.getPrice(),form.getStockQuantity());
return "redirect:/items";
}
- 새로 생성 된 item객체는 new를 통해 새로 생성된 객체이고, 아직 영속성 컨텍스트에는 등록되지 않았지만, 식별자를 가지고 있다.
- 이처럼 임의로 엔티티 객체를 만들어 내도 식별자를 가지고 있으면 준영속 엔티티라 한다.
변경감지(영속 상태 만들기)
- 영속성 컨텍스트에 속해있으려면 해당 데이터를 select해준다.
- select문을 통해서 영속성 컨텍스트에서 가져올건데, 영속성 컨텍스트에 데이터가 없으면 DB에서 가져온다.
- select 요청을 함으로써, 영속성 컨텍스트영역으로 요청을 보내서 그 안에 내 정보가 담기게된다.
- findItem 이라는 객체는 영속 상태에 놓이게 된다.
Item findItem = itemRepository.findOne(itemid);
@Transactional
public void updateItem(Long itemid, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemid);
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
- 준영속 상태인 객체를 영속상태로 바꾸고 그 영속상태인 데이터를 가지고
set
만을 해주면 변경감지를 통해 update
가 수행된다.
Transaction
이 종료되는순간 자동으로 변경된 부분을 감지해서 update
를 수행한다.
이처럼 변경점을 감지해서 update
하는 것을 변경 감지라 한다.
🧩 Domain
@Entity
@Getter @Setter
public class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if(restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
}
🧩 DTO
@Getter @Setter
public class ItemForm {
private Long id;
private String name;
private int price;
private int stockQuantity;
}
🧩 Web
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<form th:action="@{/items/new}" th:object="${itemForm}" method="post">
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요">
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br />
<div th:replace="fragments/footer :: footer" />
</div>
</body>
</html>
itemList
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>상품명</th>
<th>가격</th>
<th>재고수량</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.id}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.price}"></td>
<td th:text="${item.stockQuantity}"></td>
<td>
<a th:href="@{/items/{id}/edit(id=${item.id})}"
class="btn btn-primary" role="button">수정</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer" />
</div>
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<form th:object="${form}" method="post">
<input type="hidden" th:field="*{id}">
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" />
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요" />
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div th:replace="fragments/footer :: footer" />
</div>
</body>
</html>
🧩 Controller
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping(value = "items/new")
public String createForm(Model model) {
model.addAttribute("itemForm", new ItemForm());
return "items/createItemForm";
}
@PostMapping("/items/new")
public String create(ItemForm itemform) {
Item item = new Item();
item.setName(itemform.getName());
item.setPrice(itemform.getPrice());
item.setStockQuantity(itemform.getStockQuantity());
itemService.saveItem(item);
return "redirect:/";
}
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
@GetMapping("/items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId")Long itemId, Model model){
Item item = itemService.findOne(itemId);
ItemForm form = new ItemForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
model.addAttribute("form", form);
return "items/updateItemForm";
}
@PostMapping("/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") Item form) {
Item item = new Item();
item.setId(item.getId());
item.setName(item.getName());
item.setPrice(item.getPrice());
item.setStockQuantity(item.getStockQuantity());
itemService.updateItem(itemId,form.getName(),form.getPrice(),form.getStockQuantity());
return "redirect:/items";
}
}
🧩 Service
ItemService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
@Transactional
public void updateItem(Long itemid, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemid);
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
}
🧩 Repository
ItemRepository
@Repository
@RequiredArgsConstructor
public class ItemRepository {
@Autowired
private final EntityManager em;
public void save(Item item) {
em.persist(item);
}
public List<Item> findAll() {
return em.createQuery("select i from Item i", Item.class).getResultList();
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
}