1. @Query

Repository의 메서드 위에 선엄함으로써 sql 쿼리를 작성할 수 있습니다. 명명법으로 해결하기 힘든 쿼리를 사용할 때 유용합니다.

1-1. 기본 사용법

메서드 위에 @Query(sql)을 해주시면 됩니다.

@Query("select m from Member m")
List<Member> findMemberBy();
  • @Query 안에 들어가는 테이블은 별칭을 부여해서 사용할 수 있습니다.
  • Member 객체를 m이라는 별칭을 이용했습니다.
@Test
@DisplayName("@Query 사용 basic")
void selectAll(){
    List<Member> members = createMember();

    for(Member member: members){
        memberRepository.save(member);
    }

    List<Member> memberList = memberRepository.findMemberBy();

    for(Member member: memberList){
        System.out.println(member);
    }

    assertThat(memberList.size()).isEqualTo(4);
}

1-2. 인자 넘기기

메서드로 넘어온 인자들을 쿼리로 넘겨주는 방법은 총 3가지가 존재합니다. 

Name과 Age를 넘기는 방법을 알아보겠습니다.

1. 메서드 인자 순서

@Query("select m from Member m where m.name = ?1 and m.age = ?2")
Optional<Member> findMemberToSequenceByNameAndAge(String name, int age);

2. 메서드 인자 이름 그대로 적용 

@Query("select m from Member m where m.name = :name and m.age = :age")
Optional<Member> findMemberToTargetNotDesignateByNameAndAge(String name, int age);

3. 메서드 인자 이름 변경하여 적용

@Query("select m from Member m where m.name = :name and m.age = :age")
Optional<Member> findMemberToTargetDesignateByNameAndAge(@Param("name") String n, @Param("age") int a);

2. Update

find와 delete가 있으니 Update도 명명법으로 해결할 수 있을 것 같습니다. 하지만 Update는 명명법으로 지원하지 않습니다. 그렇기에 @Query를 사용하는 방법이 있습니다. 

@Query("update Member m set m.age=m.age+1 where m.age>:age")
int bulkByAgeGreaterThanPlus(int age);

인자로 들어온 age보다 나이가 큰 Member들에 대해서 나이를 한 살 증가시킵니다. 

@Test
@DisplayName("update 사용 @Modifying 없을 때 오류 발생")
void updateNotModifying(){
    List<Member> members = createMember();

    for(Member member: members){
        memberRepository.save(member);
    }

    assertThatThrownBy(() -> memberRepository.bulkByAgeGreaterThanPlus(11))
            .isInstanceOf(InvalidDataAccessApiUsageException.class);
}

하지만 이렇게 작성한다면 오류가 발생합니다. 그 이유는 Dirty Checking이 아닌 여러 건의 벌크 연산을 수행할 경우 영속성 콘텍스트에 있는 1차 캐시를 사용하지 않고 DB에 직접 쿼리를 날리기 때문에 예외를 발생시킵니다.

 

예외 발생을 막기 위해선 @Modifying을 붙여줘야 합니다.

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

그리고 또 해줘야 할 부분이 있습니다. 위에서 말씀드렸다시피 1차 캐시를 사용하지 않고 DB에 직접 쿼리를 날리기 때문에 1차캐시를 비워줘야 합니다. 안 비워줬을 경우 다음과 같이 1차캐시와 DB가 동기화 되지 않은 모습을 볼 수 있습니다. 

@Test
@DisplayName("update 사용 @Modifying 있을 때 속성 x")
void updateModifyingButNotAttribute(){
    List<Member> members = createMember();

    for(Member member: members){
        memberRepository.save(member);
    }
	
    // m1 나이는 12살이며, findAll()을 통해서 1차캐시에 Member들의 Entity존재
    List<Member> all = memberRepository.findAll();

	// DB에 직접 쿼리를 날려 m1의 나이를 13살로 증가
    int i = memberRepository.bulkModifyingByAgeGreaterThanPlus(11);

	// 1차캐시에는 m1에 대한 정보가있으므로 1차캐시에서 가져옴
    Member result = memberRepository.findByName("m1").get();

	// DB에는 13살이지만, 1차캐시에는 12살이므로 테스트를 통과함
    assertThat(result.getAge()).isEqualTo(12);

}

따라서 1차 캐시를 비워주는 설정을 부여해야합니다. 그것을 @Modifying으로 할 수 있습니다. 

bulk연산이 실행된 후 1차캐시를 비워주는 속성인 clearAutomatically를 true로 설정해주시면 됩니다. default는 false입니다. 

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age=m.age+1 where m.age=:age")
int bulkModifyingAttributeByAgeGreaterThanPlus(int age);
@Test
@DisplayName("update 사용 @Modifying 있을 때 속성 o")
void updateModifyingAndAttribute(){
    List<Member> members = createMember();

    for(Member member: members){
        memberRepository.save(member);
    }

    List<Member> all = memberRepository.findAll();

    int i = memberRepository.bulkModifyingAttributeByAgeGreaterThanPlus(11);

    Member result = memberRepository.findByName("m1").get();

    assertThat(result.getAge()).isEqualTo(13);

}

테스트가 성공한 것을 볼 수 있습니다.


정리하기

1. 명명법으로 복잡한 쿼리를 @Query로 해결할 수 있고 인자로 넘기는 방법을 알아봤습니다.

1-1. 메서드 인자 순서

1-2. 메서드 인자 이름 그대로 적용 

1-33. 메서드 인자 이름 변경하여 적용

2. Update쿼리에 대해 알아봤고, @Modifying의 필요성에 대해서 알아봤습니다. 

2-1. Update 쿼리는 1차캐시를 거치지 않고 DB에 직접 쿼리를 날린다.

2-2. 따라서 Update 쿼리를 날리기 위해선 @Modifying이 동반돼야 한다.

2-3. Update 쿼리 수행 후 1차 캐시와 DB의 동기화를 위해 1차 캐시를 다 비워줘야 한다.

2-4. 이때 @Modifying의 clearAutomactically = true 속성을 줌으로써 편리하게 할 수 있다. 

 

 

'SpringBoot > JPA' 카테고리의 다른 글

Spring-Data-JPA [6] Index 적용하기  (3) 2022.05.16
Spring-Data-JPA [5] Fetch Join  (0) 2022.03.24
Spring-data-JPA [3] 명명법  (0) 2022.03.22
Spring-data-JPA(2) 개념  (0) 2022.01.06
Spring-data-JPA(1) [엔티티] Entity  (0) 2022.01.06