스프링 데이터 JPA-19.Update 쿼리 메소드
스프링 데이터 JPA: Update 쿼리 메소드
쿼리 생성하기
- find…
- count…
- delete…
- 흠.. update는 어떻게 하지?
Update 또는 Delete 쿼리 직접 정의하기
- @Modifying @Query
- 추천하진 않습니다.(밑의 설명 참조)
@Modifying
- clearAutomatically
- flushAutomatically
Update 쿼리 메소드 사용
private Post savePost() {
Post post = new Post();
post.setTitle("spring start jpa");
Post savedPost = postRepository.save(post);
return savedPost;
}
@Test
public void updateTitle(){
Post spring = savePost();
int update = postRepository.updateTitle("hibername", spring.getId());
assertEquals(update, 1);
}
@Modifying
@Query("UPDATE Post p SET p.title = ?1 WHERE p.id = ?2")
int updateTitle(String title, Long id);
하지만 Update 쿼리 메소드 사용 시에는 주의해야 할 점이 있다.
아래 테스트는 실패한다.
@Test
public void updateTitle(){
Post spring = savePost();
int update = postRepository.updateTitle("hibername", spring.getId());
assertEquals(update, 1);
Optional<Post> byId = postRepository.findById(spring.getId());
assertEquals(byId.get().getTitle(), "hibername"); // 실패한다. 왜?
}
테스트 로그를 보면
(update 만 한다. select 안함.)
비록 데이터베이스 업데이트 쿼리를 발생했지만,
아직 퍼시스턴트 상태인 객체는 그대로 캐시에 남아 있었기 때문이다.
Persistence Context = 영속성 컨텍스트
엔티티 매니저 내부에 영속성 컨텍스트가 있다고 보면 된다.
Insert를 한 뒤에 커밋을 하지 않고 Select를 하면 데이터가 나온다.
그러면 이 데이터는 어디에 저장된 것일까?
그것이 바로 캐시이다.
즉, 실제 데이터베이스에 저장되진 않았지만 임시적으로 중간(캐시)에 저장된다.
이것이 바로 영속성 컨텍스트이다.
캐시에서 이렇게 데이터베이스로 합쳐서 넣을 수 있는 상태(커밋하면 데이터베이스로 들어갈 수 있는 데이터 상태)를 영속 상태라 한다.
트랜잭션을 위해 이 기능을 이용한다.
캐시에는 데이터가 아니고 객체형태로 들어간다
이런 문제때문에 스프링에서도 방법을 제시한다.
- clearAutomatically (업데이트를 실행하고 난 후 영속성 컨텍스트 초기화)
업데이트를 실행한 다음 퍼시스턴트 안에 있던 캐시를 비워준다. 왜 비워주냐면 비워 줘야 이 객체가 findById할때 새로 읽어오니까. 비워 주지 않으면 가지고 있던 캐시에서 읽는다.(위 실패한 테스트 참조)
- flushAutomatically (업데이트를 실행하기 전 영속성 컨텍스트 초기화)
업데이트를 실행하기 전에. 그 동안에 퍼시스턴트 컨텍스트에 쌓어있던 데이터 변경사항이 있었다면 먼저 반영해준다.
@Modifying(clearAutomatically = true)
@Query("UPDATE Post p SET p.title = ?1 WHERE p.id = ?2")
int updateTitle(String title, Long id);
위 테스트에서 clearAutomatically를 true해 주면 테스트는 성공한다. 테스트 로그를 보면 (update 후 다시 select 한다)
그런데 이런 방법은 크게 추천하지 않는다.
더 간편한 방법이 있다.
@Test
public void updateTitle(){
Post spring = savePost();
spring.setTitle("hibernate");
List<Post> all = postRepository.findAll();
assertEquals(all.get(0).getTitle(), "hibernate");
}
명시적으로 update 쿼리를 날리진 않았지만.
find하기 전 하이버네이트가 이 반영사항을 수정해줘야 한다는 걸 안다.
즉 이상태에서 실행하면, update 후 select 하는 로그를 확인할 수 있다.
하지만 어쩔수 없이 사용을 해야 할 경우도 있다. 추가적으로 공부를 좀 하다가 더 자세한 설명을 찾아서 정리해 본다.
참조
- https://github.com/cheese10yun/TIL/blob/master/Spring/jpa/jpa.md#%EB%B2%8C%ED%81%AC-%EC%97%B0%EC%82%B0-%EC%A3%BC%EC%9D%98%EC%A0%90
벌크 연산
수백 개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸린다. 이 때 여러 건을 한번에 수정하거나 삭제하는 벌크 연산을 사용 할 수 있다. ~~~java // 재고가 10개 미만인 모든 상품의 가격을 10% 상승시키는 업데이츠 public void bulkUpdate() { String sql = “update Product set p.price = p.price * 1.1 where p.stockAmount < :stockAmount”;
int resultCount = em.createQuery(sql) .setParameter(“stockAmount”, 10) .executeUpdate(); }
// 100원 미만 상품을 삭제하는 코드 public void bulkDelete() { String sql = “delete from Product p where p.price < :price”
int resultCount = em.createQuery(sql)
.setParameter("price", 100)
.executeUpdate(); }
// JPA 표준은 아니지만 하이버네이트는 INSERT 벌크 연산도 지원한다. // 100원 미만의 만든 상품을 선태갷서 ProducutonTemp에 저장한다 public void bulkInsert() { String sql = “insert into ProductTemp(id, price, stockAmount) select p.id, p.name, p.price, p.stockAmount from Product p where p.price < :price”
int resultCount = em.createQuery(sql)
.setParameter("price", 100)
.executeUpdate(); } ~~~ 벌크 연산은 executeUpdate() 메서드를 사용한다. 이 메서드는 별크 연산으로 영향을 받은 엔티티 건수를 반환한다.
벌크 연산 주의점
- 벌크 연산을 사용할 때 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 한다.
// 벌크 연산 시 주의점 예제
public void bulkTest() {
// (1) 상품A의 가격은 1000 이다.
Product product = em.createQuery("select p from Product p where p.name = :name", Product.class)
.setParameter("name", "productA")
.getSingleResult();
// 출력 결과 : 1000
System.out.println("Product 수정전 : " + productA.getPrice());
// (2) 벌크 연산 수행으로 모둔 상품 가격 10% 상승
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
// (3) 출력 결과 : 1000
System.out.println("Product 수정후 : " + productA.getPrice());
}
(1) 가격이 1000원인 상품A를 조회했다. 조회된 상품A는 영속성 컨텍스트에서 관리 된다.
(2) 벌크 연산으로 모든 상품의 가격을 10% 상승시켰다. 따라서 상품A의 가격은 1100원이 되어야 한다.
벌크 연산을 수행한 후에 상품 A의 가격을 출력하면 기대했던 1100원이 아니라 1000원이 출력된다.
벌크 연산 수행전
- 상품 A를 조회했으므로 가격이 1000원인 상품 A가 영속성 컨텍스트에 괸리
벌크연산 수행 후
- 벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 쿼리한다.
- 따라서 영속성 컨텍스트에 있는 상품A와 데이터베이스에 있는 상품A의 가격이 다를 수 있다.
- DB에만 접근해서 데이터를 바꾸지만, 영속성 컨텍스트가 관리하고 있는 데이터도 건드려서 수정하지 않는다.
벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true) 옵션을 true로 지정하면 된다. 저런 부분을 방지하기 위해 스프링 데이터 JPA에서 제공 하는 기능이다.