Hibernate/JPA(EntityManager)

[JPA] UPDATE 변경감지와 병합 (DirtyChecking / Merge)

유혁스쿨 2023. 7. 14. 10:51
728x90
반응형

엔티티는 영속상태로 관리된다.

이렇게 영속상태에서 관리되는 엔터티의 값을 변경하게 되면 JPA는 Transaction Commit시점에 변경된 내용을 확인하고 Database에 반영한다.

이것을 '변경감지' 'DirtyChecking' 이라고 한다.

 

변경 감지(DirtyChecking) : Transaction commit시점에 엔티티의 변경을 감지해서 updateQuery를 날려준다.

@SpringBootTest
@Transactional
public class ItemUpdateTest {

    @PersistenceContext
    EntityManager em;
    
    @Test
    @Rollback(false)
    public void updateTest() throws Exception {

        Book saveBook = new Book();
        saveBook.setName("hi");
        em.persist(saveBook);
        em.flush();
        em.clear();

        Book findBook = em.find(Book.class, saveBook.getId());

        //Tx
        findBook.setName("asdasda"); // 이름 변경
    
     }
 }

 

 

준 영속 엔티티란?

영속성 컨텍스트가 더는 관리하지 않는 엔터티를 말한다.

아래 예시를 보면 Book 객체는 이미 Database에 한번 저장되어서 식별자가 존재한다.

이렇게 임의로 만들어 낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
https://www.inflearn.com/questions/70393/book-%EA%B0%9D%EC%B2%B4%EA%B0%80-%EC%99%9C-%EC%A4%80%EC%98%81%EC%86%8D%EC%9D%B8%EA%B2%83%EC%9D%B8%EA%B0%80

 

 

준영속 엔티티 UPDATE 방법 2가지

  1. 변경감지(Dirty Checking) 기능 사용
  2. 병합(merge) 사용

 

변경감지(DirtyChecking) - 영속 엔티티

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final EntityManager em;

    @Transactional(readOnly = false) //트랜잭션
    @PostMapping("/items/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") Long itemId, BookForm form) {
    
        //영속 엔티티 - @Transactional에 의해 변경감지확인.
        Book findBook = (Book) itemRepository.findOne(itemId); 
        
        findBook.setId(itemId);
        findBook.setName(form.getName());
        findBook.setPrice(form.getPrice());
        findBook.setStockQuantity(form.getStockQuantity());
        findBook.setAuthor(form.getAuthor());
        findBook.setIsbn(form.getIsbn());
        
    }
    
}
  1. @Transactional에 의해 Commit이 된다.
    1. JPA에서 flush한다. (영속성 컨텍스트의 변경내용을 데이터에베이스에 반영)
      1. Entity와 1차캐시의 SnapShot 비교 (영속화 최초시점에 SnapShop을 찍는다)
      2. 변경감지후 쓰기지연 SQL 저장소에  UPDATE Query 저장
    2. flush에 의해 쓰기지연 SQL 저장소로 부터 Query 호출(UPDATE Query 호출)

 

 

[변경감지 구현 예시코드]

@Controller
@RequiredArgsConstructor
public class ItemController {
    
    @PostMapping("/items/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") Long itemId, BookForm form) {
    
        itemService.updateItem(itemId, form);
        return "redirect:/items";
        
    }
}

/* =================================================================================== */

@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    @Transactional(readOnly = false) // 트랜잭션
    public void updateItem(Item item) {
        Book findBook = (Book) itemRepository.findOne(itemId); //영속 엔티티 - @Transactional에 의해 변경감지확인.
        findBook.setId(itemId);
        findBook.setName(form.getName());
        findBook.setPrice(form.getPrice());
        findBook.setStockQuantity(form.getStockQuantity());
        findBook.setAuthor(form.getAuthor());
        findBook.setIsbn(form.getIsbn());
    }
}

 

병합(Merge) - 준영속 엔터티

병합이란 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다.

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final EntityManager em;

    @PostMapping("/items/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") Long itemId, 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());
        
        if(book.getId() != null) { //Book의 ID가 null이 아니면
            Item item = em.merge(book); //영속화
        }
        
    }
    
}

위와 같이 merge하게되면 매개변수로 넘긴 book을 영속화 하고 값을 변경시켜 DB에 반영한다.

반영 이후 영속화 된 객체(item)을 반환받는다.

        Book findBook = (Book) itemRepository.findOne(itemId); 
        
        findBook.setId(itemId);
        findBook.setName(form.getName());
        findBook.setPrice(form.getPrice());
        findBook.setStockQuantity(form.getStockQuantity());
        findBook.setAuthor(form.getAuthor());
        findBook.setIsbn(form.getIsbn());

변경감지의 예시인 위의 코드와 같이 값을 변경해주는작업을 하는것이다.

하지만 위 코드는 변경을 감지하고 변경사항이 있으면 UPDATE쿼리를 날리는 매커니즘이지만, 병합(Merge)의 경우 자동으로 변경감지가 이루어지지 않기 때문에 PK(id)기준으로 null 체크를한다. 

null체크를 하여 값이 존재하는 경우는 이미 데이터베이스에서 저장되었던 데이터 이기 때문에 변경하겠다는 처리이다.

 

 

병합 단점 및 주의사항

변경감지 기능을 활용하는 영속엔터티의 경우 원하는 값만 변경이 가능한 반면,

준영속 엔터티는 특정 필드의 데이터가 null인경우 null로 업데이트 해버린다.

다시말해 원하는 값만 변경하면 그 외의 변경하지 않은 값들은 null로 업데이트된다.

따라서 병합은 반드시 모든 필드를 교체해야한다.

(변경하지 않는 필드는 기존 값을 직접 다시 주입해줘야한다.)

 

[병합 구현 예시 코드]

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    @PostMapping("/items/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") Long itemId, BookForm form) {
        
        Book book = new Book(); //준영속 엔티티 - merger를 통해 수정해야함.
        book.setId(itemId);
        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);

     }
 }

/* =================================================================================== */

@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    @Transactional(readOnly = false) // 트랜잭션
    public void saveItem(Item item) {
        itemRepository.save(item);
    }
}

/* =================================================================================== */

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item) {
        if (item.getId() == null) { //item id가 존재하지 않으면 영속성 컨텍스트에 등록
            em.persist(item);
        } else { //item id가 존재한다면 merge(수정)?
            em.merge(item);
        }
    }
728x90
반응형