TIL

쿼리 메소드 기능 2

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

페이징과 정렬 파라미터

패키지를 보면 두 인터페이스 모두 JPA가 아니다. 관계형 DB든 몽고 DB든 페이징을 공통화 시킨 것이다.

특별한 반환 타입

페이징과 정렬 사용 예제

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 사용 예제 실행 코드

@Test
public void page() throws Exception {
		//given
		memberRepository.save(new Member("member1", 10));
		memberRepository.save(new Member("member2", 10));
		memberRepository.save(new Member("member3", 10));
		memberRepository.save(new Member("member4", 10));
		memberRepository.save(new Member("member5", 10));

		//when
		PageRequest pageRequest = PageRequest.of(
			0, 3, // 0페이지에서 3개 가져와
			Sort.by(Sort.Direction.DESC, "username")
		);
		Page<Member> page = memberRepository.findByAge(10, pageRequest);

		//then
		List<Member> content = page.getContent(); //조회된 데이터
		assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
		assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
		assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
		assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
		assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
		assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}

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); //변환기
}

Page 반환 사용 시 주의 사항

@Query(value = select m from Member m,
			 countQuery = select count(m.username) from Member m)
Page<Member> findMemberAllCountBy(Pageable pageable);

페이지를 유지하면서 엔티티를 DTO로 변환하기

Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

벌크성 수정 쿼리

JPA를 사용한 벌크성 수정 쿼리

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

스프링 데이터 JPA를 사용한 벌크성 수정 쿼리

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

@EntityGraph

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

페치 조인 사용 코드

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

@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)

JPA Hint & Lock

JPA Hint

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

아무런 정보(힌트)가 없으면 JPA는 영속성 컨텍스트 안에서 스냅샷을 이용하여 엔티티의 변경을 항상 감지하고 변경 감지를 실행해야 한다. (비용이 든다.)

조회만 하고 싶다면 변경 감지를 동작시킬 필요가 없다.

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
@Query("select m from Member m where m.username = :username")
Member findReadOnly(@Param("username") String username);

실제로 전체 기능 중 조회 용 메서드에 전부 readOnly 설정을 해줘봐야야 성능 개선이 얼마 되지 않는다. 성능 문제가 되는 건 진짜 복잡한 조회 쿼리 몇 개가 문제다. 복잡하고 트래픽이 많은 API 몇 개에 넣는 거지 싹 다 넣는 건 번거로울 수 있다. (성능 테스트를 먼저 해보자)

성능 툴: apache ab, jmeter, ngrinder(http://naver.github.io/ngrinder/

스프링5.1 버전 이후부터는 @Transactinal(readOnly=true) 어노테이션을 설정하면 @QueryHint의 readOnly도 자동으로 동작된다.

LOCK

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);