스프링 데이터 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에서 제공 하는 기능이다.




© 2019. by jaeuk