Tech/JPA
Spring Data JPA @Modifying 알아보기
주지민
2021. 10. 4. 17:11
반응형
벌크 연산 관련 공부를 하다가 @Modifying에 대해 자세히 알아볼 필요가 생겨 공부한 내용입니다
참고 블로그
1. @Modifying 이란?
- @Query 어노테이션(JPQL Query, Native Query)을 통해 작성된 INSERT, UPDATE, DELETE(SELECT 제외) 쿼리에서 사용되는 어노테이션입니다
- 기본적으로 JpaRepository에서 제공하는 메서드 혹은 메서드 네이밍으로 만들어진 쿼리에는 적용되지 않습니다
- clearAutomatically, flushAutomatically 속성을 변경 할 수 있으며 주로 벌크 연산과 같이 이용됩니다
- clearAutomatically ( default: false )
- flushAutomatically ( default: false )
- 예제 코드
// MemberInfoRepository extends JpaRepository<MemberInfo, Long> @Query("UPDATE MemberInfo m " + "SET m.name = :name " + "WHERE m.age >= :age") void updateNameByAgeGreaterThan(@Param("name") String name, @Param("age")int age);
- 위와 같은 조건에 맞는 모든 로우에 벌크 UPDATE를 하기위해 @Query 어노테이션을 이용하여 JPQL을 정의했습니다
- 실행 결과
- 잘 실행될 거라고 기대했지만 위와 같은 에러가 뜨게되고, 공식문서를 확인해보니 아래와 같이 @Query 어노테이션 선언을 통해 DML(INSERT, UPDATE, DELETE)문을 실행할 경우 반드시 @Modifying을 붙이라고 강제하고 있습니다
Queries that require a `@Modifying` annotation include INSERT, UPDATE, DELETE, and DDL statements.
2. @Query + @Modifying 적용
- org.springframework.data.jpa.repository.Modifying
- 다시 정의한 JPQL에 @Modifying을 선언하고 다시 실행해보겠습니다
- 예제 코드
// Repository public interface MemberInfoRepository extends JpaRepository<MemberInfo, Long> { @Query("UPDATE MemberInfo m " + "SET m.name = :name " + "WHERE m.age >= :age") @Modifying void updateNameByAgeGreaterThan(@Param("name") String name, @Param("age") int age); }
- 테스트 코드
// test @SpringBootTest @Transactional public class ModifyingTest { @Autowired private MemberInfoRepository memberInfoRepository; @BeforeEach void setUp() { // given memberInfoRepository.save(new MemberInfo(10, "주지민1", "서울")); memberInfoRepository.save(new MemberInfo(20,"주지민2", "춘천")); memberInfoRepository.save(new MemberInfo(30, "주지민3", "판교")); memberInfoRepository.save(new MemberInfo(40,"주지민4", "서울대입구")); memberInfoRepository.save(new MemberInfo(50,"주지민5", "강남")); memberInfoRepository.save(new MemberInfo(60,"주지민6", "건대입구")); } @Test @Commit void bulkUpdateMemberTest() { // when memberInfoRepository.updateNameByAgeGreaterThan("계란한판이상멤버", 30); // then List<MemberInfo> memberInfos = memberInfoRepository.findAll(); memberInfos.forEach(System.out::println); } }
- 주지민1 ... 주지민6까지 총 6개의 데이터가 영속성 컨텍스트에 저장
- 정의한 updateNameByAgeGreaterThan() 메서드를 통해 30살 이상의 멤버 name을 "계란한판이상멤버"로 벌크 처리
- findAll()를 통해 모든 MemberInfo 로우를 가져와 출력
- 실행 결과
- 첫번째 결과는 Test Case의 맨마지막 콘솔 출력이고, 두 번째 결과는 데이터베이스 결과값입니다
- 실제 데이터베이스 결과는 정상적으로 업데이트되었지만, 같은 영속성 컨텍스트에 저장되어있는 값은 변경이 안된 것을 확인할 수 있습니다
3. 결과 분석
- @Query로 정의된 벌크 연산 JPQL은 기존 JPA처럼 영속성 컨텍스트를 거쳐 쓰기지연SQL로 동작하는 것이 아닌 바로 Database에 질의를 하게 됩니다
- 실행 순서
- 테스트 코드에서 save(@Id sequence table 사용)를 통해 INSERT한 6개의 Entity는 바로 쿼리 실행이 되지않고 영속성 컨텍스트와 쓰기지연SQL에 INSERT 쿼리를 넣습니다
- updateNameByAgeGreaterThan()이 호출되고 기존에 쓰기지연SQL에 저장되었던 Query들이 flush됩니다 ( 이건 뒤에서 다시 설명하겠습니다 )
- updateNameByAgeGreaterThan()에서 @Query로 정의된 JPQL이 실행되고 DB에 반영
- findAll()을 통해 모든 데이터를 불러와서 콘솔에 출력
- 원인 분석
- 원하는대로 결과가 안나왔던 이유는 실행 순서 3번과 4번에서 찾을 수 있었습니다
- @Query로 정의된 JPQL이 직접 DB에 쿼리를 날리게되고, findAll()을 통해 실제 모든 데이터를 불러왔지만 이미 같은 @Id로 영속성 컨텍스트에 저장되있는 데이터는 무시하게됩니다 ( 즉 영속성컨텍스트의 정의되어있는 데이터가 우선순위를 갖고, 덮어쓰지 않는다 )
- 따라서 실제 변경된 데이터가 영속성 컨텍스트에 저장되지 못하고, 기존 데이터가 출력되게 됩니다
4. 해결책: clearAutomatically
- 이럴경우 이름에서 어느정도 느낌이 오듯 @Modifying 속성중 하나인 clearAutomatically를 true로 변경해주면 해결됩니다
- clearAutomatically는 @Query로 정의된 JPQL을 실행후에 자동으로 영속성 컨텍스트를 비워주는 옵션입니다. 영속성 컨텍스트가 비워지니 데이터베이스에서 가져온 모든 데이터들이 영속성컨텍스트에 저장되어 최신상태를 유지할 수 있습니다
- @Modifying 속성 변경
// Repository @Query("UPDATE MemberInfo m " + "SET m.name = :name " + "WHERE m.age >= :age") @Modifying(clearAutomatically = true) void updateNameByAgeGreaterThan(@Param("name") String name, @Param("age") int age);
- 실행 결과
- 드디어 원하는 결과가 나왔습니다~!!!
5. 추가: flushAutomatically
- 아까 동작 순서를 확인하다보면 이상한 부분이 있습니다
- 분명 @Modifying의 속성중 flushAutomatically의 default값은 false인데 updateNameByAgeGreaterThan 메서드를 실행하기전에 flush가 알아서 동작한 부분입니다
- 정답은 Spring Data JPA의 구현체인 Hibernate default 설정에 있습니다
- Hibernate는 flushModeType 설정을 전역으로 할 수 있는데 두가지 타입을 가지고 있습니다
- FlushModeType enum class
- AUTO: flush가 쿼리 실행시 발생 (default)
- COMMIT: flush가 트랜잭션이 COMMIT될때 발생
- default가 AUTO, 즉 실제 쿼리가 발생할때마다 영속성 컨텍스트에 flush가 동작되게 설정되어있고 이때문에 @Modifying flushAutomatically 속성이 false이여도 쿼리실행전에 flush가 동작했습니다
- spirng.jpa.properties.org.hibernate.flushMode=[AUTO/COMMIT] 로 변경할 수 있습니다
728x90
반응형