데이터베이스의 데이터값들을 한번에 바꿀때 사용하는 쿼리가 벌크성 수정 쿼리이다.
간단한 비교를 위해 순수 JPA과 Spring Data JPA코드를 비교해보겠다.
//순수 JPA - 벌크 연산
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();
}
//Spring Data JPA - 벌크 연산
@Modifying(clearAutomatically = true) //순수 JPA 에서 "executeUpdate()" 역할
@Query("update Member m set m.age=m.age+1 where m.age>= :age")
int bulkAgePlus(@Param("age")int age);
순수 JPA 코드
의 업데이트 쿼리는 어렵지 않은 수정 쿼리이다.
순수 JPA 코드에서 수정, 삭제 쿼리시에 executeUpdate()
를 사용해야 되고, 업데이트된 데이터의 수를 리턴해주게 된다.
스프링 데이터 JPA 코드
에서 @Modifying
는 앞선 순수 JPA코드의 excuteUpdate()
의 기능을 하기 때문에 반드시 적어준다.
간단하지만 JPA의 벌크연산시 주의해야할 점이 있다!!
벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트의 1차 캐시에 엔티티들의 상태와 DB에 엔티티의 상태가 다를수 있다❗
//Spring Data JPA - 벌크 연산
@Test
public void 벌크연산테스트() 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
Assertions.assertThat(resultCount).isEqualTo(3);
//벌크연산 주위사항!
Member member = memberRepository.findByUsername("member5").get(0);
Assertions.assertThat(member.getAge()).isEqualTo(41);
}
간단한 테스트 코드를 보게되면,
commit 시점에 save한 member들을 DB로 쿼리문을 날리고 영속성 컨텍스트 1차 캐시에 계속 존재하게 된다.
그리고 벌크 연산이 실행되면 영속성 컨텍스트를 거치지 않고 DB로 쿼리문 바로 날리게 된다.
-> 실제 DB에서는 값이 바뀌어 있음!!
이렇게 되면, 벌크 연산을 적용한 엔티티들은 영속성 컨텍스트 1차 캐시에는 예전값이 존재하고 DB에는 수정한 최신값이 존재하게 된다.❗
만약, 벌크연산 이후에 트랜잭션이 종료가 되면 상관없지만 추가 로직이 존재하여 수정했던 엔티티를 검색하게 되면 영속성 컨텍스트 1차 캐시에 있는 값을 주기 때문에 결국 수정되기전 값이 리턴되게 된다.
그렇기 때문에, 벌크 연산이후에는 영속성 컨텍스트의 1차 캐시를 반드시 비워야된다.
이러한 기능을, Spring Data JPA
에서 @Modifying(clearAutomatically = true)
의 옵션을 통해
직접 flush() 호출 없이 자동으로 벌크 연산이후 영속성 컨텍스트 1차 캐시를 비워준다.!!
JPA
사용 시 많이들 발생하는 문제이자 큰 문제인 N+1
문제가 있다. 이를 해결하기 위한 방식에는 fetch join
, betch
등이 있다.
참고💡 : 예전 JPA 강의 들으며 정리한 N+1, fetch join에 대한 내용이다.
https://github.com/BonSik-Koo/backend_study/blob/main/basic/Jpa/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80%20%EC%A6%89%EC%8B%9C%EB%A1%9C%EB%94%A9%2C%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9.md
N+1
문제를 해결하기 위한 방법 중 fetch join
는 연관관계가 있는 엔티티들을 하나의 쿼리로 한번에 가져오는것이다. 즉 EntityGrapth들을 한번에 가져오는것이다.
스프링 데이터 JPA
에서 fetch join()을 사용하는 방식은 여러가지가 있다. 아래와 같이 보자
//Spring Data JPA - 1.엔티티그래프(fetch join 해주는것) - JPQL 방식
@Query("select m from Member m left join fetch m.team t")
List<Member> findMemberFetchJoin();
//Spring Data JPA - 2.엔티티그래프(fetch join 해주는것) - 메서드 이름 + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
List<Member> findMemberEGByUsername(String username);
//Spring Data JPA - 3.엔티티그래프(fetch join 해주는것) - JPQL + 엔티티 그래프
@Query("select m from Member m")
@EntityGraph(attributePaths = {"team"})
List<Member> findMemberJPQL_EG();
//Spring Data JPA - 4.엔티티그래프(fetch join 해주는것) - "공통 인터페이스 방식"을 오버라이드 (이건별로..)
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
직접 JPQL
에 fetch을 사용하는 방식 -> 쿼리가 복잡해지면 보통 쓰는 방식이라고 함.
메서드 이름으로 쿼리 생성
을 사용한 방식 -> 간단한 쿼리 일경우 사용
JPA
가 Member 엔티티를 가져올때 left join과 fetch을 통해 Team 엔티티도 같이 가져옴.
참고로
fetch join
은left join
으로 실행된다.
JPQL 과 엔티티 그래프 기능을 혼합하여 사용하는 경우
동작 방식은 2과 같음
공통 인터페이스 기능
을 오버라이딩하여 사용한 방식
JPA 구현체인 Hibernate
에게 제공하는 힌트
순수 조회용으로 사용할 때 성능 최적화를 위해 사용
기존에 변경감지기능을 사용하기 위해서 영속성 컨텍스트에 변경전 데이터를 스냅샷으로 저장하여 데이트 변동시 스냅샷과 비교하여 감지하지만, 이 기능을 사용하면 스냅샷을 만들지 않아 시간/메모리 면에서 최적화 가능
하지만, 보통 복잡한 쿼리에서 성능이 떨어지는 경우가 많기 때문에 이러한 기능을 사용한 최적화는 큰 효과를 보지 못할수 있다. 이러한 방법이 있다는 것만 알아놔야 겠다!
//Spring Data JPA - ReadOnly
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
//Spring Data JPA - ReadOnly
@Test
public void ReadOnly() throws Exception {
//given
memberRepository.save(new Member("member1",10));
em.flush(); //DB와 동기화
em.clear(); //영속성 컨텍스트 1차 캐시 초기화
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //업데이트 쿼리가 발생하지 않는다!!
}