[Spring Data JPA] 2-2. JPA 페이징과 정렬
JPA 페이징과 정렬
Spring Data JPA는 페이징 및 정렬 기능을 지원한다.
순수 JPA 페이징과 정렬
아래의 조건으로 페이징과 정렬을 사용해 보겠다
- 검색 조건 : 나이가 10살
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
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();
}
위와 같이 페이징을 할 때 필요한 offset과 limit로 잘라서 가져오는 쿼리와 totalCount를 가져오는 두 가지의 쿼리를 작성했다.
이를 테스트해보면 아래와 같다
@Test
public void paging() {
//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 = 1;
int limit = 3;
// when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
// then
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
Spring Data JPA 페이징과 정렬
Spring Data JPA에서는 페이징 및 정렬 기능을 제공한다.
페이징과 정렬 관련 파라미터
- org.springframework.data.domain.Sort : 정렬을 위한 클래스
- org.springframework.data.domain.Pageable : 페이징 기능을 제공. 내부에 Sort를 포함하고 있음.
반환 타입에 따른 특징
- org.springframework.data.domain.Page : 추가 count 쿼리의 결과를 포함하는 페이징 (ex: 페이지 내릴 때, "더보기"를 누르면 다음 결과들이 이어서 출력됨)
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1 조회)
- List : 추가 count 쿼리 없이 결과만 반환
페이징과 정렬 사용 예제
페이징을 사용하여 나이가 10살인 member 중, 이름으로 내림차순으로 정렬하겠다. 단, 페이지 조건은 첫 번째 페이지, 페이지당 보여줄 데이터는 3건이다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
@Test
public void paging() {
//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));
// 페이징 및 정렬 조건
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// 0페이지에서 3개 가져와라. sort의 방향은 DESC(정렬 조건은 선택)
// when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
// then
List<Member> content = page.getContent(); // page 내부에 있는 3개(pageSize)를 꺼내옴
assertThat(content.size()).isEqualTo(3); // 페이지 사이즈
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0); // 페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2);// 총 페이지 개수(알아서 계산됨) - 페이지 사이즈는 3인데 member가 5명이니깐 2가 나와야됨
assertThat(page.isFirst()).isTrue(); // 첫 페이지인가??
assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있나?? (게시판의 다음페이지, 이전페이지 같은데서 사용)
}
Pageable은 인터페이스이므로, org.springframework.data.domain.PageRequest 객체를 사용한다. PageRequest 생성자의 첫 번째 파라미터는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터의 수를 입력한다. 페이지는 0부터 시작한다.
다음으로는 Slice를 사용해보겠다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Slice<Member> findByAge(int age, Pageable pageable); // 페이징 슬라이스
}
@Test
public void paging() {
//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));
// 페이징 및 정렬 조건
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// 0페이지에서 3개 가져와라. sort의 방향은 DESC(정렬 조건은 선택)
// when
Slice<Member> page = memberRepository.findByAge(age, pageRequest); // Slice로 바꿔야 됨
// then
List<Member> content = page.getContent(); // page 내부에 있는 3개(pageSize)를 꺼내옴
assertThat(content.size()).isEqualTo(3); // 페이지 사이즈
assertThat(page.getNumber()).isEqualTo(0); // 페이지 번호
assertThat(page.isFirst()).isTrue(); // 첫 페이지인가??
assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있나?? (게시판의 다음페이지, 이전페이지 같은데서 사용)
}
결과 쿼리를 보면 limit가 4개인 것을 확인할 수 있다. 이는 우리가 요청한 것보다 1개 더 요청한 것이다. 이는 모바일 디바이스에서 많이 사용하며 리스트를 내리다 보면 만약에 조회할 내용이 하나 더 있으면 더보기 버튼이라든지 로딩을 한다는지 할 때 사용하는 방법이다.
타입만 Slice로 바꾸면 간단하게 사용이 가능하다.
다음으로는 List를 적용해보겠다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByAge(int age, Pageable pageable);
}
@Test
public void paging() {
//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));
// 페이징 및 정렬 조건
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// 0페이지에서 3개 가져와라. sort의 방향은 DESC(정렬 조건은 선택)
// when
List<Member> page = memberRepository.findByAge(age, pageRequest); // List로 수정
}
리스트를 사용하는 경우는 단순히 리스트를 개수 단위로 끊어서 가져오고 싶을 때 사용한다.
복잡한 쿼리의 경우 countQuery를 사용하여 분리
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
}
User findTopByOrderByAgeDesc()나 User findFirstByOrderByLastnameAsc(); 처럼 Top, First와 같은 키워드를 사용하여 결과의 제한된 수를 조회할 수 있다.
더 자세한 정보는 https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.limit-query-result 에서 참고할 수 있다.
페이지를 유지하면서 엔티티를 DTO로 변환
// when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null)); // Page를 유지하면서 Dto로 변환 가능
[출처]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84 → 이 글은 김영한님의 "실전! 스프링 데이터 JPA" 강의 중 5장을 듣고 정리한 내용입니다.