스프링데이터 + JPA/스프링 데이터 JPA

17. 벌크성 수정쿼리

sdafdq 2023. 11. 23. 08:06

우리가 기존 수정은 데이터를 가져온 다음,

그걸 트랜잭션 내부에서 엔티티의 값을 변경을 하면 알아서 update 쿼리가 commit 시점에 나간다.

 

근데, 만약 예를 들어 전체 회사원의 연봉을 10% 올리고 싶다면?

 

이렇게 한번에 bulk(통으로)적으로 수정하는 걸 벌크성 수정 쿼리라고 한다.

 

먼저 순수 JPA에서

public int bulkAgePlus(){
    return em.createQuery("update Member m set m.age= m.age + 3")
            .executeUpdate();
}

그냥 통으로 update 시켜 버리면 됨.

조건 넣고 싶을 경우 인자로 받던지 해서 넣으면 되고,

그리고 나름 중요한 부분이, executeUpdate() 저 부분. 저걸 해야 영향받은 row의 개수를 반환해 줌.

아니면 기본값이 getResultList()나 singResult임.

 

@Test
public void bulkUpdate(){
    memberJpaRepository.save(new Member("member1", 10));
    memberJpaRepository.save(new Member("member2", 10));
    memberJpaRepository.save(new Member("member3", 10));
    memberJpaRepository.save(new Member("member4", 10));
    memberJpaRepository.save(new Member("member5", 10));

    int updatedCount = memberJpaRepository.bulkAgePlus(0);

    List<Member> findMembers = memberJpaRepository.findAll();
    System.out.println("updatedCount = " + updatedCount);
    for (Member findMember : findMembers) {
        System.out.println("findMember age = " + findMember.getAge());
    }
}

테스트.

다 13됨.

 

근데 이거 테스트 클래스에 트랜잭션이 달려있는거라서,

저렇게 findAll 해와도 영속성 컨텍스트에 있는건 영속성 컨텍스트에서 먼저 뒤져서 가지고 오는거라,

반영이 안되어 있어서,

public List<Member> findAll(){
    em.clear();
    return em.createQuery("select m from Member m", Member.class)
            .getResultList();
}

이렇게 비워놨음.

그냥 잠깐동안 볼려고 저렇게 해 놓은거.

 

이거 차라리 위에 정의해 놓은 벌크쿼리에,

그러니까 

public int bulkAgePlus(){
    int updatedCount = em.createQuery("update Member m set m.age= m.age + 3")
            .executeUpdate();
    em.clear();
    return updatedCount;
}

 

이런 식으로 해 놓는게 훨씬 좋음.

어차피 쿼리 나갈 때는 JPA가 알아서 flush() 해서 (개발자가 직접 작성한 쿼리는 어떤 쿼리일 지 모르니, 일단은 쿼리로 직접 나가는 건 나가기 전에 flush()해서 기존에 쌓아놓은 지연쿼리들 다 DB에 보내고 이 쿼리도 바로 날림. 앞서 얘기한 이유와 마찬가지로 이 쿼리도 바로 날리는 거임.)

나가기 때문에 flush()를 임의로 호출해 줄 필요는 없지만,

영속성은 비워둬야 함.

그래야 select 했을 때 잘 가져옴. 아니면 영속성 컨텍스트에 있는 반영안된 엔티티를 가져올 수도.. (개발자가 어떤 쿼리를 날릴 지 모르니 섣부르게 영속성컨텍스트에 반영할 수는 없음. 비효율적이기도 하고.)

그래서 만약 그냥 저렇게 update 쿼리 날리고 끝낼거면 모르겠는데, 저렇게 벌크 한 다음 조회해와서 뭐 할 계획이면은, 저렇게 해 주는게 좋음.

 

벌크 쿼리 날리면 DB에는 반영이 되었는데, 영속성 컨텍스트는 반영이 안되어 있으니, 그냥 비워버리고 처음부터 다시 가져오게끔.

 

여튼 변경되는 건 잘 봤음.

 

그러면 Spring Data JPA에서는 어떻게 하냐?

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

이번엔 인자로 받은 age 이상이어야만 바뀌게 해 봤음.

근데 저 @Modifying 저건 뭐냐? 

직역 그대로 수정 이라는 뜻 인데, 저걸 하면 아, 개발자가 수정쿼리를 내보냈구나 하고 영향받은 쿼리의 수를 return 시키도록 함. 안 그러면 위처럼 getResultList()나 getSingResult()를 함.

저 clearAutomatically가 뭐냐?

예상한 그대로, 자동으로 clear, 영속성 컨텍스트를 clear 해준다는 거임.

 

@Test
public void bulkUpdate(){
    memberRepository.save(new Member("member1", 9));
    memberRepository.save(new Member("member2", 14));
    memberRepository.save(new Member("member3", 5));
    memberRepository.save(new Member("member4", 26));
    memberRepository.save(new Member("member5", 22));

    int updatedCount = memberRepository.bulkAgePlus(15);

    List<Member> findMembers = memberRepository.findAll();
    for (Member findMember : findMembers) {
        System.out.println("findMember = " + findMember.getAge());
    }

    assertThat(updatedCount).isEqualTo(2);
}

15살 이상만 바뀌는 거 확인 함.

clearAutomatically로 자동으로 영속성 컨텍스트가 clear 되어서 새로 가져와 제대로 나오는 것도 확인 함.

 

벌크 쿼리 날릴 때는,

@Modifying(clearAutomatically = true)

이렇게 붙여줘서 영속성 컨텍스트 clear 해 주기.

 

그냥 붙여 놓는게 여러모로 좋을 듯. 그러니까 붙여 놓는게 신경 안써도 될 듯.