2 분 소요


목차

  • 벌크성 수정 쿼리
  • @EntityGraph
  • JPA Hint & Lock


벌크성 수정 쿼리

순수 JPA

public int bulkAgePlus(int age) {
    int resultCount = em.createQuery(
            "update Member m set m.age = m.age + 1" +
            " where m.age >= :age")
            .setParameter("age", age)
            .executeUpdate();
    return resultCount;
}

스프링 데이터 JPA

@Modifying // executeUpdate()와 같은 역할을 한다.
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용
    • 사용하지 않으면 다음 예외 발생
    • org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
  • 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화 : @Modifying(clearAutomatically = true)
    • 이 옵션의 기본값은 false
    • 이 옵션 없이 회원을 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 만약 다시 조회해야 하면 반드시 영속성 컨텍스트를 초기화해야 한다.

[참고]
벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB의 엔티티 상태가 달라질 수 있다.
[권장하는 방안]

  1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
  2. 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.


@EntityGraph

연관된 엔티티들을 SQL 한번에 조회하는 방법

@Test
void findMemberLazy() {
    // given
    // member1 -> teamA
    // member2 -> teamB
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    teamRepository.save(teamA);
    teamRepository.save(teamB);
    memberRepository.save(new Member("member1", 10, teamA));
    memberRepository.save(new Member("member2", 20, teamB));

    em.flush();
    em.clear();

    // when
    List<Member> members = memberRepository.findAll();

    // then
    for(Member member : members) {
        member.getTeam().getName();
    }
}

member -> team은 지연로딩이기 때문에, team의 데이터를 조회할 때 마다 쿼리가 실행된다.(N + 1)

연관된 엔티티를 한번에 조회하려면 패치 조인이 필요하다.

JPQL 패치 조인

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();

스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용할 수 있도록 도와준다. 이 기능을 사용하면 JPQL 없이 패치 조인을 사용할 수 있다.(JPQL + 엔티티 그래프도 가능)

EntityGraph

// 공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

// JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

// 메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username);

EntityGraph 정리

  • 사실상 패치 조인(FETCH JOIN)의 간편 버전
  • LEFT OUTER JOIN 사용

NamedEntityGraph (중요X)

@NamedEntityGraph(name = "Member.all", attributePaths = 
@NamedAttributeNode("team"))
@Entity
public class Member {}

@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();


JPA Hint

JPA Hint

JPA 쿼리 힌트(SQL 힌트가 아닌 JPA 구현체에게 제공하는 힌트)

쿼리 힌트 사용

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);

쿼리 힌트 사용 확인

@Test
void queryHint() {
    
    memberRepository.save(new Member("member1", 10));
    em.flush();
    em.clear();

    Member member = memberRepository.findReadOnlyByUsername("member1");
    member.setUsername("member2");

    em.flush(); // Update Query 실행 X
}

쿼리 힌트 Page 추가 예제

@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")},
            forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
  • org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
  • forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true)


<출처 : 인프런 - 실전! 스프링 데이터 JPA(김영한)>

댓글남기기