본문 바로가기
JPA

[JPA] 벌크성 수정 쿼리

by jinjin98 2022. 12. 4.

벌크성 수정 쿼리

 

데이터베이스 쿼리문을 작성하다 보면 대량의 데이터를 한꺼번에 수정하는 경우가 있습니다.

쇼핑몰로 예를 들면 1월 1일에 모든 회원의 나이를 한 살씩 증가시키거나

이벤트로 모든 회원에게 포인트를 추가해줄 수 있습니다.

 

JPA 를 사용해서 이런 벌크성 수정 쿼리를 작성하면 조심해야할 점들이 몇 가지 있는데요,

이번 포스팅에서는 이 부분에 대해서 알아보겠습니다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {

        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;

        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

 

예제에 사용할 Member 엔티티입니다.

 

@Repository
public class MemberJpaRepository {

    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 로 벌크성 수정 쿼리를 작성했습니다. 회원이 파라미터로 들어온 나이와 같거나 더 많은 나이를 갖고있다면

나이를 1살 더해주는 쿼리입니다. executeUpdate() 메서드를 사용하면 수정된 회원의 개수를 반환해줍니다.

 

@Test
public void bulkUpdate() throws Exception {
    //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 = memberJpaRepository.bulkAgePlus(20);

    //then
    assertThat(resultCount).isEqualTo(3);
}

 

 

작성한 메서드로 테스트 코드를 작성해보았습니다. 나이가 10, 19, 20, 21, 40 인 회원들을 저장한 후,

나이가 20살 이상인 회원들은 모두 나이가 1살씩 증가하게 되어 총 3명의 회원이 수정 쿼리가 적용됩니다.

 

public interface MemberRepository extends JpaRepository<Member, Long> {

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

 

이번엔 스프링 데이터 JPA 로 벌크 수정 쿼리를 작성했습니다. 스프링 데이터 JPA 로 벌크성 수정, 삭제 쿼리를 

작성할 때는 @Modifying 어노테이션을 무조건 사용해야 합니다.  사용하지 않으면 

org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations 예외가 발생합니다.

 

@Test
public void bulkUpdate() throws Exception {
    //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
    List<Member> result = memberRepository.findByUsername("member5");
    Member member5 = result.get(0);
    System.out.println("member5 의 나이 = " + member5.getName());
}

 

테스트 코드를 이전과 다르게 작성해보겠습니다.

벌크성 수정 쿼리를 실행하고 회원 중에 회원 이름이 member5 인 회원의 나이를 조회해보겠습니다.

 

 

회원의 나이가 1 증가해 41이 나올줄 알았지만 그대로 40이 출력됩니다. 

어떻게 된 것일까요?

JPA 에는 영속성 컨텍스트가 존재합니다. 자바와 데이터베이스 사이에서 1차 캐시 역할을 하는데요.

조회를 하면 데이터베이스에서 값을 가져오기 전 영속성 컨텍스트에서 조회하려는 값을 찾아 가져오고 

영속성 컨텍스트에 값이 없다면 그 때 데이터베이스에서 값을 가져오고, 값을 가져오면서 영속성 컨텍스트를 거치기

때문에 조회하는 값이 영속성 컨텍스트에도 저장됩니다. 저장은 데이터베이스에 값을 저장하기 전에 

영속성 컨텍스트에 값을 저장합니다. 추후에 영속성 컨텍스트 플러시가 실행되면 INSERT 쿼리가 실행되어

영속성 컨텍스트에 있는 값들을 데이터베이스에 저장하면서 됩니다. 수정 같은 경우에는영속성 컨텍스트 플러시가

실행될 때 변경 감지(더티 체킹) 기능이 동작해 이전에 영속성 컨텍스트에 저장된 값들과 비교해 변경된 부분이 있으면

UPDATE 쿼리가 실행됩니다. 플러시는 JPQL 을 실행, 커밋이 수행될 때, 직접 flush() 메서드를 실행할 때 일어납니다.

 

여기서 주의해야할 점이 벌크성 수정 쿼리는 변경 감지 기능이 동작하지 않는 것인데요.

위 테스트 코드에서 회원 엔티티를 저장하면 영속성 컨텍스트에서도 저장되고 데이터베이스에도 저장됩니다.

그 다음 벌크성 수정 쿼리를 실행하면 영속성 컨텍스트의 엔티티 관리를 무시하고 바로 데이터베이스에 UDDATE

쿼리를 실행합니다. 이렇게 되면 영속성 컨텍스트에 저장된 값은 아무런 변경이 일어나지 않습니다.

이 떄 회원의 이름이 member5 인 회원을 조회하면 데이터베이스에서 값을 조회하기 전에 영속성 컨텍스트를 

먼저 조회하므로 변경되지 않은 값인 40을 조회하게 되는 것이죠.

 

이 문제를 해결하기 위해서는 영속성 컨텍스트를 비워줘야 합니다.

영속성 컨텍스트를 비워주면 조회를 할 때 영속성 컨텍스트에 값이 없으므로

데이터베이스에서 값을 조회하게 됩니다.

영속성 컨텍스트의 내용을 비울 때는 영속성 컨텍스트인 EntityManager 클래스의 clear() 메서드를 사용하거나,

@Modifying 어노테이션의 clearAutomatically 옵션값을 true 로 해주면 됩니다.

 

@Test
public void bulkUpdate() throws Exception {
    //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.clear();     

    //then
    List<Member> result = memberRepository.findByUsername("member5");
    Member member5 = result.get(0);

    System.out.println("member5 의 나이= " + member5.getAge());
}

 

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

 

 

영속성 컨텍스트를 비워주고 값을 조회하니 41이 출력된 걸 확인할 수 있습니다.

이렇게 벌크성 수정 쿼리를 실행하고 나서는 영속성 컨텍스트의 값과 데이터베이스의 값이 맞지 않을 수 있기 때문에

영속성 컨텍스트를 비워줘야 합니다. 현재처럼 벌크성 수정 쿼리를 실행한 후 JPA 를 사용해서 데이터베이스에서

값을 조회하는 방법 말고도 JDBC 템플릿이나 MyBatis 같이 다른 데이터베이스 연동기술을 이용해서 값을 조회해도

영속성 컨텍스트를 비워줘야 합니다.

댓글