2 분 소요

JPA를 사용한 벌크성 수정 쿼리

JPA에서 전체 회원의 나이를 바꿔야 한다면?

  • 20살 이상의 회원의 나이를 + 1 해라

순수 JPA

1
2
3
4
5
6
public int bulkAgePlus(int age) {  
    return em.createQuery("update Member m set m.age = m.age+1" +  
                    " where m.age >=:age")  
            .setParameter("age", age)  
            .executeUpdate();  
}

다음과 같이 age를 받아서 age 이상인 것만 +1 업데이트 문을 날리도록 만들었다.

테스트 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test  
public void bulkUpdate() throws Exception {  
    //given  
    memberJpaRepository.save(new Member("member1", 10));  
    memberJpaRepository.save(new Member("member2", 12));  
    memberJpaRepository.save(new Member("member3", 20));  
    memberJpaRepository.save(new Member("member4", 25));  
    memberJpaRepository.save(new Member("member5", 30));  
    memberJpaRepository.save(new Member("member6", 50));  
    //when  
    int result = memberJpaRepository.bulkAgePlus(20);  
    //then  
    assertThat(result).isEqualTo(4);  
}

멤버를 여러 개 만들고 20을 했더니 결과가 4가 나왔고 통과가 잘 됐다.

DB에서 확인해보면

원하는 대로 +1이 된 걸 볼 수 있다.

이제 스프링 데이터 JPA에서 어떻게 하는 지 알아보자.

스프링 데이터 JPA

스프링 데이터 JPA에서도 @Query어노테이션을 이용하여 순수 JPA에서 사용한 쿼리를 그대로 사용한다.

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

다음과 같이 age를 @Param으로 잡고, @Query에 쿼리를 작성했다. 그리고 추가 된 것이 @Modifying어노테이션이다. 수정 작업이 있다면 반드시 줘야 한다.

순수 JPA에서도 .executeUpdate(); 한 것과 비슷하다고 보면 된다.

이제 테스트 코드를 순수 JPA와 같이 사용해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test  
public void bulkUpdate() throws Exception {  
    //given  
    memberRepository.save(new Member("member1", 10));  
    memberRepository.save(new Member("member2", 12));  
    memberRepository.save(new Member("member3", 20));  
    memberRepository.save(new Member("member4", 25));  
    memberRepository.save(new Member("member5", 30));  
    memberRepository.save(new Member("member6", 50));  
    //when  
    int result = memberRepository.bulkAgePlus(20);  
    //then  
    assertThat(result).isEqualTo(4);  
}

다음과 같이 했고 통과도 잘 됐고 결과도

다음과 같이 잘 나왔다.

그런데 벌크 업데이트에선 주의해야 할 게 있다. 벌크 업데이트는 DB에 직접 업데이트를 치는 것이므로 영속성 컨텍스트와 무관하게 동작한다.

벌크 업데이트 주의 사항

자 예를 들어 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test  
public void bulkUpdate() throws Exception {  
    //given  
    memberRepository.save(new Member("member1", 10));  
    memberRepository.save(new Member("member2", 12));  
    memberRepository.save(new Member("member3", 20));  
    memberRepository.save(new Member("member4", 25));  
    memberRepository.save(new Member("member5", 30));  
    memberRepository.save(new Member("member6", 50));  
    //when  
    int result = memberRepository.bulkAgePlus(20);  

	Member findMember = memberRepository.findById(6L).get();  
	System.out.println("findMember = " + findMember);

    //then  
    assertThat(result).isEqualTo(4);  
}

이런 코드가 있을 때 과연 member6의 나이가 50일까 51일까?

정답은 50이다.

벌크 연산 자체가 영속성 컨텍스트를 건드리지 않고 직접 DB에 업데이트를 친 행위 이고, 그 전에 memberRepository.save(new Member("member6", 50)); 하면서 1차 캐시에 이 값을 저장해 놓았기 때문에

1
2
Member findMember = memberRepository.findById(6L).get();  
System.out.println("findMember = " + findMember);

다음과 같은 행위를 했을 때 DB를 거치지 않고 1차 캐시에서 값을 가져오게 된 것이다.

그러므로 반드시 벌크 업데이트 작업이 끝난 후에 엔티티 매니져를 날려 줘야 한다. 이와 마찬가지로 MyBatis나 JDBC템플릿을 섞어서 사용한다면 이 또한 JPA는 인식하지 못한다.

1
2
em.flush();  
em.clear();

이 두 코드를 추가 하고 다시 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test  
public void bulkUpdate() throws Exception {  
    //given  
    memberRepository.save(new Member("member1", 10));  
    memberRepository.save(new Member("member2", 12));  
    memberRepository.save(new Member("member3", 20));  
    memberRepository.save(new Member("member4", 25));  
    memberRepository.save(new Member("member5", 30));  
    memberRepository.save(new Member("member6", 50));  
    //when  
    int result = memberRepository.bulkAgePlus(20);  
    em.flush();  
    em.clear();  
    Member findMember = memberRepository.findById(6L).get();  
    System.out.println("findMember = " + findMember);  
  
    //then  
    assertThat(result).isEqualTo(4);  
}

실행 해보면

다음과 같이 51살로 바뀐 걸 볼 수 있다.

또 스프링 데이터 JPA에서는 em.flush(), clear()할 필요 없이 @Modifying어노테이션에 기능이 있다.

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

다음과 같이 clearAutomatically = true를 넣어주면 엔티티 매니저를 대신 날려준다.

태그: ,

카테고리:

업데이트:

댓글남기기