Spring Data JPA 게시글은 대부분 인프런의 김영한님의 강의인 '실전! 스프링 데이터 JPA' 기반으로 내용을 정리했습니다.
벌크성 수정 쿼리
JPA는 엔티티를 가져와서 변경할 경우 변경 감지 기능이 작동한다. 이런 경우는 한 건 씩 지원되는 거고, 모든 데이터에 일괄적인 업데이트를 날려야 하는 경우(ex 모든 직원의 연봉 10% 인상)에 벌크성 수정 쿼리라고 한다.
순수 JPA 예제
- 20살이거나, 20살 이상인 회원의 나이를 +1
- MemberJpaRepository.class
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(); // 응답 값의 개수 나옴
}
- MemberJpaRepositoryTest.class
@Test
public void bulkUpdate() {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 19));
memberJpaRepository.save(new Member("member3", 20));
memberJpaRepository.save(new Member("member4", 21));
memberJpaRepository.save(new Member("member5", 40));
// when
int resultCount = memberJpaRepository.bulkAgePlus(20);
// then
assertThat(resultCount).isEqualTo(3);
}
update
member
set
age=age+1
where
age>=?
Spring Data JPA 예제
Spring Data JPA에서는 @Modifying 애노테이션과 반환 값이 int이기만 하면 된다.
- MemberRepository.interface
@Modifying // jpa executeUpdate 같은 기능 필수임
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- MemberRepositoryTest.class
@Test
public void bulkUpdate() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
// then
assertThat(resultCount).isEqualTo(3);
}
update
member
set
age=age+1
where
age>=?
벌크성 수정, 삭제 쿼리는 @Modifying 애노테이션을 사용해야 한다. 사용하지 않으면 org.hibernate.hql.internal.QueryExceptionRequestException: Not supported for DML operations 에러가 발생한다. 그러므로 꼭 필수로 넣어줘야 하는 애노테이션이다.
벌크성 쿼리의 주의할 점
JPA를 사용하면 엔티티들은 영속성 컨텍스트에서 관리가 된다. 벌크성 쿼리는 이 영속성 컨텍스트를 무시하기 때문에 영속성 컨텍스트에 있는 엔티티의 값들과 DB에 있는 값이 달라질 수 있다.
- MemberRepositoryTest.class
@Test
public void bulkUpdate() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
List<Member> result = memberRepository.findByUsername("member5");
Member member = result.get(0);
System.out.println("member = " + member);
// then
assertThat(resultCount).isEqualTo(3);
}
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username=?
member = Member(id=5, username=member5, age=40)
위 테스트를 실행하면 충격적이게도 member에 있는 값이 41이 아니라 40으로 출력된다. 이렇게 DB와 영속성 컨텍스트에 있는 값이 다를 수 있다. 이런 경우를 방지하기 위해 벌크 연산 후에는 영속성 컨텍스트를 아예 초기화시켜서 영속성 컨텍스트에서 DB의 값을 가져오도록 해야 한다.
영속성 컨텍스트를 초기화하기 위해 맨 윗줄에 EntityManager를 가져올 수 있도록 코드를 추가한다.
@PersistenceContext
EntityManager em;
@Test
public void bulkUpdate() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
// em.flush(); // 남아있는 변경되지 않는 내용을 DB에 반영. 여기서는 굳이 안 해도 된다.
em.clear(); // 초기화!
List<Member> result = memberRepository.findByUsername("member5");
Member member = result.get(0);
System.out.println("member = " + member);
// then
assertThat(resultCount).isEqualTo(3);
}
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username=?
member = Member(id=5, username=member5, age=41)
Spring Data JPA 영속성 컨텍스트 초기화
Spring Data JPA에서는 굳이 em.clear() 명령을 적을 필요가 없다. 그러므로 주석 처리 후 아래 코드를 추가하자.
- MemberRepository.interface
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Modifying 애노테이션에 clearAutomatically 속성을 true로(기본값은 false) 주면 굳이 따로 em.clear()를 하지 않아도 자동으로 update 쿼리가 발생된 후 영속성 컨텍스트를 초기화해준다.
위 문제점에 대한 방안
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
- 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화한다.