벌크성 수정 쿼리 주의점

루민 ·2023년 5월 7일
0

📝벌크성 수정 쿼리

  • Spring Data Jpa를 배우고 공부하던 도중에 주의할 점이 있어 기록하려고 합니다.

📝Member

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})  //team 은 출력 X!!! 연관 관계에 의한 무한 루프 발생
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, int age) {
        this.username = userName;
        this.age = age;
    }

    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.getMemberList().add(this);
    }
}

📝벌크 수정 쿼리(MemberRepository)

 	@Modifying
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);
  • bulkAgePlus Query : 파라미터로 받은 나이이거나 그 나이 이상이면 한 살을 더해주도록 하였습니다.

📝MemberRepositoryTest

 	@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", 50));

        //when
        int resultCount = memberRepository.bulkAgePlus(20);   //벌크 연산 실행 직전 flush() 자동 호출

        Member findMember = memberRepository.findByUsername("member5").orElseGet(() -> null);
        System.out.println(findMember);

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

  • 'member5'로 예를 들겠습니다.
  • 처음에 member5의 age가 51로 예상했습니다.
  1. memberRepositoy.save (em.persist)로 영속성 컨텍스트에 member5가 저장

    영속성 컨텍스트데이터베이스
    usernamemember5
    age50
  2. memberRepository.bulkAgePlus(20)(JPQL) 실행 전 flush() 자동 호출

    영속성 컨텍스트데이터베이스
    usernamemember5member5
    age5050
  1. memberRepository.bulkAgePlus(20) 실행 후

    영속성 컨텍스트데이터베이스
    usernamemember5member5
    age5051
  2. memberRepository.findByUsername("member5").orElseGet(() -> null);

  • 데이터 베이스에서 값을 가져오기 때문에 51로 예상하였습니다.

  • 하지만 결과는 50이었습니다.


WHY?

  • em.find()나 지연로딩을 조회할 때는 영속성 컨텍스트(1차 캐시)에서 엔티티를 찾아옵니다.
  • 반면 위의 bulkAgePlus나 findByUsername같은 JPQL은 항상 SQL로 번역되어서 데이터베이스를 통해 실행됩니다.
  • 따라서 데이터베이스에서 username="member5"인 member 데이터를 조회합니다.
  • 하지만 이때 1차 캐시에 이미 username="member5"가 있기 때문에 식별자 충돌이 일어나게 됩니다.
  • JPA는 영속성 컨텍스트의 동일성을 보장하기 때문에 1차 캐시에 있는 값을 반환하게 됩니다.
  • 그렇기 때문에 결과는 50이 나오게 됩니다.

해결방안

방법1.

 	@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", 50));

        //when
        int resultCount = memberRepository.bulkAgePlus(20);   //벌크 연산 실행 직전 flush() 자동 호출
        em.clear();

        Member findMember = memberRepository.findByUsername("member5").orElseGet(() -> null);
        System.out.println(findMember);

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

방법2.

  	@Modifying(clearAutomatically = true)
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);
  • EntityManager(em).clear를 통해 영속성 컨텍스트를 비워주는 작업을 진행합니다.
  • 그렇게되면 데이터베이스의 결과값이 바로 반환되기 때문에 결과값은 51이 됩니다!
  • Member(id=5, username=member5, age=51)

0개의 댓글