순수 JPA를 사용한 벌크 연산 쿼리는 다음과 같다.
public int bulkAgePlus(int age) {
int resultCount = em.createQuery(
"update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리는 다음과 같다.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
이때 쿼리는 getResultList()
를 사용하는게 아니라 executeUpdate()
를 사용한다. 즉 벌크 연산은 하나의 엔티티를 수정하여 변경 감지로 update 쿼리가 실행되는 변경 감지와 달리, 여러 엔티티를 한 번에 update하도록 DB에 바로 update 쿼리를 실행한다.
테스트 코드는 다음과 같다.
@SpringBootTest
class MemberTest {
@Autowired
private MemberRepository memberRepository;
@Test
@Transactional
public void bulkUpdate() {
//given
memberRepository.save(new Member("m1", 10));
memberRepository.save(new Member("m2", 19));
memberRepository.save(new Member("m3", 20));
memberRepository.save(new Member("m4", 21));
memberRepository.save(new Member("m5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20); // 수정된 엔티티의 수를 반환
//then
Assertions.assertThat(resultCount).isEqualTo(3);
}
}
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 JPQL이 실행된다. 이 점을 고려하여 다음 테스트 코드를 살펴보자.
@SpringBootTest
class MemberTest {
@Autowired
private MemberRepository memberRepository;
@Test
@Transactional
public void bulkUpdate() {
//given
memberRepository.save(new Member("m1", 10));
memberRepository.save(new Member("m2", 19));
memberRepository.save(new Member("m3", 20));
memberRepository.save(new Member("m4", 21));
memberRepository.save(new Member("m5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
Member findMember = memberRepository.findByUsername("m5");
//then
Assertions.assertThat(findMember.getAge()).isEqualTo(40);
}
}
memberRepository.bulkAgePlus(20)
를 통해서 m5의 나이를 40에서 41로 수정하였다. 그러나 이는 DB에 바로 JPQL이 실행되어 나이가 수정된 것이다. 반면 memberRepository.findByUsername("m5")
를 통해 m5를 조회해올 때는 영속성 컨텍스트에 엔티티가 있는지 조회하고, 아직 플러시가 되지 않았기 때문에 영속성 컨텍스트의 m5를 반환한다. 따라서 이때 m5의 나이는 여전히 40이다.
이렇게 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 JPQL을 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티 상태와 DB에 있는 엔티티 상태가 달라질 수 있다. 따라서 반드시 벌크 연산 후에 영속성 컨텍스트를 초기화하는 것이 좋다.
@SpringBootTest
class MemberTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private EntityManager em;
@Test
@Transactional
public void bulkUpdate() {
//given
memberRepository.save(new Member("m1", 10));
memberRepository.save(new Member("m2", 19));
memberRepository.save(new Member("m3", 20));
memberRepository.save(new Member("m4", 21));
memberRepository.save(new Member("m5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
em.flush();
em.clear();
Member findMember = memberRepository.findByUsername("m5");
//then
Assertions.assertThat(findMember.getAge()).isEqualTo(41);
}
}
참고로 다음과 같이 JPA는 벌크 연산 후에 영속선 컨텍스트를 초기화하는 옵션을 제공한다.
@Modifying(flushAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
변경 감지는 다음과 같이 동작한다.
영속성 컨텍스트에 엔티티의 최초 상태가 복사되어 스냅샷으로 저장되어 있다.
애플리케이션 로직에서 엔티티를 수정한다. 즉 영속성 컨텍스트에는 수정된 엔티티와 수정 전 최초 상태의 스냅샷이 저장되어 있다.
트랜잭션이 커밋되거나 JPQL이 실행될 때 플러시가 호출된다.
플러시가 호출되면 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾고, 변경 사항에 따라 수정 쿼리를 생성하여 쓰기 지연 SQL 저장소에 보관한다.
쓰기 지연 SQL 저장소의 쿼리가 DB에 실행된다.
즉 변경 감지는 애플리케이션 로직에서 엔티티를 수정하면, 영속성 컨텍스트의 엔티티도 수정된다(변경된 엔티티와 스냅샷이 함께 저장됨).
@SpringBootTest
class MemberTest {
@Autowired
private MemberRepository memberRepository;
@Test
@Transactional
public void dirtyCheckingTest() {
// 저장
Member member = new Member("member", 10);
memberRepository.save(member); // 이때 엔티티가 영속성 컨텍스트에 저장된다.
// 수정
member.setAge(12); ; // 영속성 컨텍스트의 엔티티가 수정된다.
// 조회 (이때까지 여전히 플러시 호출 X, 조회는 영속성 컨텍스트에서 조회해온다.)
Member findMember = memberRepository.findByUsername("member");
Assertions.assertThat(findMember.getAge()).isEqualTo(12);
}
}
위 예제를 보면, 아직 플러시가 호출되지 않은 상태에서 엔티티를 조회한다. 이때 엔티티는 영속성 컨텍스트에 있기 때문에 영속성 컨텍스트에서 조회해온다. 회원의 나이를 12살로 변경할 때 영속성 컨텍스트의 엔티티의 나이가 12살로 변경된 것이기 때문에 테스트 결과는 성공이다.