2022. 8. 19. 16:20ㆍLanguage`/JPA
JPQL을 통해서 페이징을 하기 위해서는 [setFirstResult(), setMaxResults()]를 통해서 페이징 처리를 하였다.
그리고 정렬을 하기 위해서는 직접 SQL Query에 정렬 기준을 명시해주었다
하지만 Spring Data JPA에서는 다음 2가지 특별한 파라미터를 제공해주고, 이 파라미터들을 통해서 페이징/정렬을 굉장히 편리하게 구현할 수 있다
org.springframework.data.domain.Sort = 정렬
org.springframework.data.domain.Pageable = 페이징 기능 (내부에 Sort 포함)
페이징에 대한 return type으로는 List/Page를 사용할 수 있다.
Page는 기본적으로 Slice를 확장한 인터페이스이고, 반환타입으로 Page를 사용하게 되면 Data JPA는 "페이징 기능을 위해 검색된 전체 데이터 건수 조회를 위한 count query"를 추가적으로 날리게 된다
Pageable
페이징 테스트를 위한 Member Data
페이징 개수 = 10개
Pageable Interface는 기본적인 페이징과 관련된 설정들을 하는 인터페이스이다
인터페이스를 그대로 사용할 수는 없으니 Pageable을 구현한 "PageRequest"를 사용해야 한다
PageRequest는 of를 통해서 offset, limit, sort에 대한 정보를 설정할 수 있다.
page나 sort를 PageRequest에 direct로 설정하지 않고 따로 설정하고 싶다면 withPage, withSort를 활용하면 된다
// 메소드
@Query(value = "SELECT m FROM Member m JOIN FETCH m.team",
countQuery = "SELECT COUNT(m) FROM Member m")
Page<Member> findBy(Pageable pageable);
// [offset = 0, limit = 10, Sort = age(ASC)]
PageRequest pageRequest = PageRequest
.of(0, 10, Sort.by("age"));
// [offset = 1, limit = 10, Sort = age(ASC), username(ASC)]
PageRequest pageRequest = PageRequest
.of(1, 10, Sort.by("age"))
.withSort(Sort.by(Sort.Direction.ASC, "age").and(Sort.by(Sort.Direction.ASC, "username")));
// [offset = 2, limit = 10, Sort = username(DESC), age(ASC)]
PageRequest pageRequest = PageRequest
.of(2, 10, Sort.by(Sort.Direction.DESC, "username").and(Sort.by(Sort.Direction.ASC, "age")));
PageRequest0에 대한 페이징 결과
PageRequest1에 대한 페이징 결과
PageRequest2에 대한 페이징 결과
Slice
Slice Interface에 정의된 여러 메소드들을 하나하나 알아보자
// 메소드
@Query(value = "SELECT m FROM Member m JOIN FETCH m.team")
Slice<Member> findBySlicing(Pageable pageable);
▶ getContent()
getContent()를 통해서 [Page/Slice]에 담긴 "페이징 데이터"들을 List로 꺼내올 수 있다
// 페이징 기준
PageRequest pageRequest = PageRequest
.of(0, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData = memberRepository.findBySlicing(pageRequest);
// Slice에서 페이징 처리된 데이터들을 getContent()를 통해서 가져오기
List<Member> pagingList = pagingData.getContent();
▶ getNumber(), getNumberOfElements()
getNumber()는 "현재 페이지"를 나타내고 getNumberOfElements()는 "현재 페이지의 데이터 개수"를 의미한다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
int getNumber1 = pagingData1.getNumber(); // Page(0)의 페이지 번호
int getNumberOfElements1 = pagingData1.getNumberOfElements(); // Page(0)에 있는 데이터 개수
int getNumber2 = pagingData2.getNumber(); // Page(1)의 페이지 번호
int getNumberOfElements2 = pagingData2.getNumberOfElements(); // Page(1)에 있는 데이터 개수
- PageRequest.of(1, 10)에서 밑줄친 "11"의 의미는 Page(1)을 가져올때 데이터의 시작 Offset을 의미한다
▶ getPageable(), nextPageable(), previousPageable(), nextOrLastPageable(), previousOrFirstPageable()
getPageable()
getPageable()은 메소드명 그대로 Slice된 페이징 데이터에 대해서 "어떤 PageRequest"로 페이징해서 가져왔는지를 판별하는 메소드이다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
// PageRequest 정보
Pageable pageable1 = pagingData1.getPageable();
Pageable pageable2 = pagingData2.getPageable();
getPageable()로부터 Return받은 Pageable에 대해서 [페이지번호, 페이지인지 여부, 페이지 사이즈, ...]등의 정보를 추가적으로 꺼내서 확인할 수 있다
nextPageable()
nextPageable()은 "현재 페이징된 Page/Slice"에 대해서 다음 Page/Slice가 존재하는지 확인하는 메소드이다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
// PageRequest 정보
Pageable pageable1 = pagingData1.nextPageable();
Pageable pageable2 = pagingData2.nextPageable();
pageRequest2의 다음 Page는 위의 더미 데이터를 확인해보면 없을거라도 쉽게 추측이 가능하다.
다음 Page가 없다면 "nextPageable()"에서는 INSTANCE라는 값을 return하게 된다
previousPageable()
nextPageable()에서 다음 Page가 없으면 INSTANCE를 return하듯이 previousPageable()도 마찬가지로 이전 Page가 없으면 INSTANCE를 return할 것이라고 예측할 수 있다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
// PageRequest 정보
Pageable pageable1 = pagingData1.previousPageable();
Pageable pageable2 = pagingData2.previousPageable();
nextOrLastPageable()
nextOrLastPageable()은 다음 Page가 있으면 해당 PageRequest를 return하고, 만약 다음 Page가 없다면 현재 PageRequest를 return한다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
// PageRequest 정보
Pageable pageable1 = pagingData1.nextOrLastPageable();
Pageable pageable2 = pagingData2.nextOrLastPageable();
previousOrFirstPageable()
previousOrFirstPageable()은 이전 Page가 있으면 해당 PageReqeust를 return하고, 만약 이전 Page가 없다면 현재 PageRequest를 return한다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
// PageRequest 정보
Pageable pageable1 = pagingData1.previousOrFirstPageable();
Pageable pageable2 = pagingData2.previousOrFirstPageable();
▶ getSize()
Pageable이 unwraped이 아니라 page라면 "PageSize"를 return하고 만약 unwrap상태라면 "ContentSize"를 가져온다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 5, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
int size1 = pagingData1.getSize();
int size2 = pagingData2.getSize();
둘다 Pageable()이 존재하는 상태이므로 PageRequest에서 설정한 "page"의 size를 return하는 것을 확인할 수 있다
▶ getSort()
getSort()는 PageRequest를 보낼때 "설정한 Sort 양식"을 return하는 메소드이다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age").and(Sort.by(Sort.Direction.DESC, "username")));
PageRequest pageRequest2 = PageRequest
.of(1, 5)
.withSort(Sort.by("age").and(Sort.by(Sort.Direction.DESC, "username").and(Sort.by("team.id"))));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
Sort sort1 = pagingData1.getSort();
Sort sort2 = pagingData2.getSort();
▶ hasContent()
hasContent()란 Paging을 통해서 조회된 데이터가 있는지에 대한 여부를 return하는 메소드이다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
PageRequest pageRequest3 = PageRequest
.of(2, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
Slice<Member> pagingData3 = memberRepository.findBySlicing(pageRequest3);
boolean hasContent1 = pagingData1.hasContent();
boolean hasContent2 = pagingData2.hasContent();
boolean hasContent3 = pagingData3.hasContent();
▶ hasNext(), hasPrevious()
다음이나 이전 Page/Slice가 있는지 여부에 대한 메소드이다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1);
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2);
boolean previous1 = pagingData1.hasPrevious(); // No..
boolean next1 = pagingData1.hasNext(); // Yes!!
boolean previous2 = pagingData2.hasPrevious(); // No..
boolean next2 = pagingData2.hasNext(); // Yes!!
▶ isFirst(), isLast()
첫번째 Page/Slice인지 혹은 마지막 Page/Slice인지 판별해주는 메소드이다
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 5, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 5, Sort.by("age"));
PageRequest pageRequest3 = PageRequest
.of(2, 5, Sort.by("age"));
// Slice
Slice<Member> pagingData1 = memberRepository.findBySlicing(pageRequest1); // 첫번째 Slice
Slice<Member> pagingData2 = memberRepository.findBySlicing(pageRequest2); // 중간 Slice
Slice<Member> pagingData3 = memberRepository.findBySlicing(pageRequest3); // 마지막 Slice
boolean isFirst1 = pagingData1.isFirst();
boolean isLast1 = pagingData1.isLast();
boolean isFirst2 = pagingData2.isFirst();
boolean isLast2 = pagingData2.isLast();
boolean isFirst3 = pagingData3.isFirst();
boolean isLast3 = pagingData3.isLast();
Page
Page는 기본적인 Slice의 모든 메소드를 상속받았고 추가적으로 몇가지 메소드를 더 제공한다
@Query(value = "SELECT m FROM Member m JOIN FETCH m.team",
countQuery = "SELECT COUNT(m) FROM Member m")
Page<Member> findByPaging(Pageable pageable);
return type이 Page일 경우 "전체 데이터"에 대한 countQuery를 날려야 하기 때문에 countQuery를 명시적으로 추가해주었다
위와 같이 JPQL에 fetch join이 있을 경우 countQuery의 존재 유무에 따라 어떻게 결과가 달라지는지 확인해보자
// countQuery X
@Query(value = "SELECT m FROM Member m JOIN FETCH m.team")
Page<Member> findByPaging(Pageable pageable);
이런 오류가 발생하는 이유는 return type이 Page면 "반드시" 전체 데이터에 대한 countQuery를 날려야 하지만 countQuery의 기준이 명확하지 않기 때문에 이러한 오류가 발생하는 것이다
// 기준 Member
@Query(value = "SELECT m FROM Member m JOIN FETCH m.team",
countQuery = "SELECT COUNT(m) FROM Member m")
Page<Member> findByPaging(Pageable pageable);
// 기준 Team
@Query(value = "SELECT m FROM Member m JOIN FETCH m.team",
countQuery = "SELECT COUNT(t) FROM Team t")
Page<Member> findByPaging(Pageable pageable);
이처럼 countQuery의 기준을 명시해주면 정상적으로 쿼리가 날라간다
- 위의 쿼리는 조회의 기준 자체가 Member이므로 countQuery의 기준도 Member로 세우는 것이 더 옳다고 생각한다
▶ getTotalElements()
getTotalElements()는 response된 페이징 데이터가 몇개냐가 아니라 "전체 Page에서 전체 데이터의 개수"를 의미한다
- return type = long
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
PageRequest pageRequest3 = PageRequest
.of(2, 10, Sort.by("age"));
// Paging
Page<Member> pagingData1 = memberRepository.findByPaging(pageRequest1);
Page<Member> pagingData2 = memberRepository.findByPaging(pageRequest2);
Page<Member> pagingData3 = memberRepository.findByPaging(pageRequest3);
long totalElements1 = pagingData1.getTotalElements();
long totalElements2 = pagingData2.getTotalElements();
long totalElements3 = pagingData3.getTotalElements();
실질적으로 Page[0, 1, 2]에 담긴 데이터의 개수는 [10, 3, 0]개 이지만, getTotalElements()는 각 페이지별이 아닌 전체 페이지의 전체 데이터 개수이므로 동일하게 "13"이 return됨을 확인할 수 있다
▶ getTotalPages()
getTotalPages()는 전체 페이지의 수를 return하는 메소드이다
- return type = long
// 페이징 기준
PageRequest pageRequest1 = PageRequest
.of(0, 10, Sort.by("age"));
PageRequest pageRequest2 = PageRequest
.of(1, 10, Sort.by("age"));
PageRequest pageRequest3 = PageRequest
.of(2, 10, Sort.by("age"));
// Paging
Page<Member> pagingData1 = memberRepository.findByPaging(pageRequest1);
Page<Member> pagingData2 = memberRepository.findByPaging(pageRequest2);
Page<Member> pagingData3 = memberRepository.findByPaging(pageRequest3);
long totalPages1 = pagingData1.getTotalPages();
long totalPages2 = pagingData2.getTotalPages();
long totalPages3 = pagingData3.getTotalPages();