[실전! 스프링 데이터 JPA] 5. 쿼리 메서드 기능

jada·2024년 9월 18일
0

Spring 스터디

목록 보기
33/35

순수 JPA 페이징과 정렬

🔎 JPA에서 페이징을 어떻게 할 것인가?

  • 페이징과 정렬 조건
    - 검색 조건: 나이가 10살
    • 정렬 조건: 이름으로 내림차순
    • 페이징 조건: 첫번째 페이지, 페이지당 보여줄 데이터는 3건

JPA 페이징 리포지토리 코드


public List<Member> findByPage(int age, int offset, int limit) {
     return em.createQuery("select m from Member m where m.age = :age order by
 m.username desc")
             .setParameter("age", age)
             .setFirstResult(offset)
             .setMaxResults(limit)
             .getResultList();
}

public long totalCount(int age) {
    return em.createQuery("select count(m) from Member m where m.age = :age",
Long.class)
            .setParameter("age", age)
			.getSingleResult();

}

JPA 페이징 테스트 코드

@Test
 public void paging() throws Exception {
	//given
     memberJpaRepository.save(new Member("member1", 10));
     memberJpaRepository.save(new Member("member2", 10));
     memberJpaRepository.save(new Member("member3", 10));
     memberJpaRepository.save(new Member("member4", 10));
     memberJpaRepository.save(new Member("member5", 10));
     int age = 10;
     int offset = 0;
     int limit = 3;
     
	 //when
     List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
     long totalCount = memberJpaRepository.totalCount(age);

	//페이지 계산 공식 적용...
	// totalPage = totalCount / size ... // 마지막 페이지 ...
	// 최초 페이지 ..
	//then
     assertThat(members.size()).isEqualTo(3);
     assertThat(totalCount).isEqualTo(5);
 }

스프링 데이터 JPA 페이징과 정렬

페이징과 정렬 파라미터

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)

특별한 반환 타입

  • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1조회)
  • List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

페이징과 정렬 사용 예제

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함 List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함 List<Member> findByUsername(String name, Sort sort);

Page 인터페이스

public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

Slice 인터페이스

public interface Slice<T> extends Streamable<T> { 
    int getNumber(); //현재 페이지
    int getSize(); //페이지 크기
    int getNumberOfElements(); //현재 페이지에 나올 데이터 수
    List<T> getContent();  // 조회된 데이터
    boolean hasContent();  // 조회된 데이터 존재여부
    Sort getSort();  // 정렬 정보
    boolean isFirst();  // 현재 페이지가 첫 페이지인지 여부
    boolean isLast();  // 현재 페이지가 마지막 페이지인지 여부
    boolean hasNext();  // 다음 페이지 여부
    boolean hasPrevious();  // 이전 페이지 여부
    Pageable getPageable(); //페이지 요청 정보
    Pageable nextPageable(); //다음 페이지 객체
    Pageable previousPageable();//이전 페이지 객체
    <U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}

벌크성 수정 쿼리

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를 사용한 벌크성 수정 쿼리 테스트

 @Test
 public void bulkUpdate() throws Exception {
//given
     memberJpaRepository.save(new Member("member1", 10));
     memberJpaRepository.save(new Member("member2", 19));
     memberJpaRepository.save(new Member("member3", 20));
     memberJpaRepository.save(new Member("member4", 21));
     memberJpaRepository.save(new Member("member5", 40));
//when
     int resultCount = memberJpaRepository.bulkAgePlus(20);
//then
     assertThat(resultCount).isEqualTo(3);
 }
  • 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용
    - 사용하지 않으면 다음 예외 발생
    - org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations

  • 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화:@Modifying(clearAutomatically = true) (이 옵션의 기본값은 false )
    - 이옵션없이회원을 findById 로다시조회하면영속성컨텍스트에과거값이남아서문제가될수있다. 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자.

참고: 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

@EntityGraph

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

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 사용

JPA Hint & Lock

JPA Hint

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

쿼리 힌트 사용

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

쿼리 힌트 사용 확인

 @Test
 public void queryHint() throws Exception {
//given
     memberRepository.save(new Member("member1", 10));
     em.flush();
     em.clear();
//when
     Member member = memberRepository.findReadOnlyByUsername("member1");
     member.setUsername("member2");
em.flush(); //Update Query 실행X }

Lock

 @Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
  • org.springframework.data.jpa.repository.Lock 어노테이션을 사용
profile
꾸준히 발전하는 개발자가 되자 !

0개의 댓글