본문 바로가기
Tech/JPA

Spring Data JPA @Modifying 알아보기

반응형
벌크 연산 관련 공부를 하다가 @Modifying에 대해 자세히 알아볼 필요가 생겨 공부한 내용입니다

https://docs.spring.io/spring-data/data-jpa/docs/current/api/org/springframework/data/jpa/repository/Modifying.html

 

Modifying (Spring Data JPA 2.5.5 API)

Indicates a query method should be considered as modifying query as that changes the way it needs to be executed. This annotation is only considered if used on query methods defined through a Query annotation. It's not applied on custom implementation meth

docs.spring.io

 

참고 블로그

 

Spring Data JPA @Modifying (1) - clearAutomatically

이 글을 작성하게 된 계기는 Spring Data JPA의 @Modifying에 있는 flushAutomatically에 대해 의문점이 생겼고, 그에 대한 학습 테스트를 해보면서 였습니다. 하지만 @Modifying의 Attribute가 clearAutomaticall..

devhyogeon.tistory.com

 

Spring Data JPA @Modifying (2) - flushAutomatically

https://devhyogeon.tistory.com/4 Spring Data JPA @Modifying (1) - clearAutomatically 이 글을 작성하게 된 계기는 Spring Data JPA의 @Modifying에 있는 flushAutomatically에 대해 의문점이 생겼고, 그에 대..

devhyogeon.tistory.com


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에 질의를 하게 됩니다
  • 실행 순서
    1. 테스트 코드에서 save(@Id sequence table 사용)를 통해 INSERT한 6개의 Entity는 바로 쿼리 실행이 되지않고 영속성 컨텍스트와 쓰기지연SQL에 INSERT 쿼리를 넣습니다
    2. updateNameByAgeGreaterThan()이 호출되고 기존에 쓰기지연SQL에 저장되었던 Query들이 flush됩니다 ( 이건 뒤에서 다시 설명하겠습니다 )
    3. updateNameByAgeGreaterThan()에서 @Query로 정의된 JPQL이 실행되고 DB에 반영
    4. 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
반응형