RDB이든, Mongo 같은 DB는 모두 아래 2개만으로 Spring Data 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 쿼리 없이 결과만 반환
public interface MemberReposiotry extends JpaRepository<Member,Long> {
//이제는 Spring Data JPA로 [페이징]과 [정렬]을 구현해볼거다.
Page<Member> findByAge(int age, Pageable pageable); // 쿼리 메서드의 이름으로 Spring Data JPA가 JPQL를 만들어 준다.
}
// JPA는 첫 페이지가 1부터 시작, Spring Data JPA는 첫 페이지가 0부터 시작!!(주의!!)
// [PageRequest]는 [page]인터페이스의 구현체!!!
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));
//위 코드에 의해 기대되는 페이징 결과 : page 0 : ["member1", "member2", "member3"] (총 3게)
// page 1 : ["member4", "member5"] : 총 2개
// -> 즉 Table에서 3개씩 묶어서 1개씩의 page를 만들어 준다.
//page1,2의 총 튜플 개수 : 총 5개
Page<Member> page = repository.findByAge(age, pageRequest); // findByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌
//위 코드에서 페이징 처리된 객체(3개)를 반환한다.
List<Member> content = page.getContent();
for (Member member : content) {
System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
}
long totalCount = page.getTotalElements(); // totalCount 반환!
System.out.println("totalCount = " + totalCount);
assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호(페이지 번호는 jpa든 Spring Data든 0부터 시작)
// 1개의 페이지 내에서 시작한는 [튜플]의 [index]가 아닌, 2개의 page가 반환될 것인데, 그 페이지들 중 가장 첫번째 index(=0)을 의미!
// 총 2개의 페이지가 반환된다(page 0, page 1)
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
}
우리가 직접 JPQL을 작성하지 않았음에도, Spring Data JPA가 자동으로 메서드를 구현해 주었다.
게다가 totalCount 정보도 생성해 준다(Page 인터페이스를 사용했을 때!)
public interface MemberReposiotry extends JpaRepository<Member,Long> {
//이제는 Spring Data JPA로 [페이징]과 [정렬]을 구현해볼거다.
// 1] Page 인터페이스 : 쿼리용 SQL문 1번, totalCount 쿼리용 SQL문 1번 : 총 2번의 쿼리 발생(실행해서 SQL문 확인!게시물에서 참조)
Page<Member> findPageByAge(int age, Pageable pageable); // 쿼리 메서드의 이름으로 Spring Data JPA가 JPQL를 만들어 준다.
// 2] Slice 인터페이스
Slice<Member> findSliceByAge(int age, Pageable pageable);
// totalCount 계산은 부하가 크다. 왜냐하면, DB Table 전~체를 다 조회해야 하기 때문인데, 상황에 따라서 totalCount 계산을 최적화 해줘야 한다.
// 우리가 쿼리 문을 날리게 되면, 대게의 경우, JOIN연산이 일어난다.
// 여기서는 Member -> Team( 다 : 1 )과 left Outer Join으로 예시를 들겠다.
// 만약, 2개의 Table 사이에 left outer join이 일어나게 된다면, 그 결과의 table의 totalcount나 member table의 total count나 값이 같다.
// -> 이러한 점들을 이용해서 Spring Data JPA는 @Query( countQuery = " select ~~~ " ) 식으로 totalcount용 쿼리를 따로 짤 수 있게 해놓음
@Query(value = "SELECT m FROM Member m LEFT JOIN m.team t", // 원래는 여기에서 totalCount 쿼리 계산도 같이 일어남!
countQuery = "SELECT COUNT(m.username) FROM Member m") // 이 부분에서 left outer Join으로 totalCount를 계산하지 않고,
// Member Table 하나만을 가지고, totalCount를 계산한다. 결과는 똑같이 나온다.
// 실행해서 sql문 확인해봐라(countQuery가 있고, 없고의 SQL문 차이 확인)
Page<Member> findtotalCountByAge(int age, Pageable pageable);
}
@Test@Transactional
public void pagin(){
/**
* 1. Page 사용 예시
*/
/*repository.save(new Member("member1",10));
repository.save(new Member("member2",10));
repository.save(new Member("member3",10));
repository.save(new Member("member4",10));
repository.save(new Member("member5",10));
int age =10;
// PageRequest는 인터페이스인 Pageable의 구현체!!!
// JPA는 첫 페이지가 1부터 시작, Spring Data JPA는 첫 페이지가 0부터 시작!!(주의!!)
// [PageRequest]는 [page]인터페이스의 구현체!!!
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));
// 쿼리 메서드용 SQL문 1번, totalCount용 SQL문 1번 : 총 2번의 쿼리리
Page<Member> page = repository.findByAge(age, pageRequest); // findByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌
//위 코드에서 페이징 처리된 객체(3개)를 반환한다.
List<Member> content = page.getContent();
for (Member member : content) {
System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
}
long totalCount = page.getTotalElements(); // totalCount 반환!
System.out.println("totalCount = " + totalCount);
assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();*/
/**
* 2. Slice 사용 예시 : 이건 [페이징] 기능을 하는 것이 전혀 아니다.
* -> 다른 곳에서 [페이징] SQL문을 날린 후, 1개의 튜플들을 더 들고 오는 것에 불과하다.
*/
/* repository.save(new Member("member1",10));
repository.save(new Member("member2",10));
repository.save(new Member("member3",10));
repository.save(new Member("member4",10));
repository.save(new Member("member5",10));
int age =10;
// [페이징] 요청은 여기서 일어남.
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));
// 참고로, Slice 클래스는 PageRequest 클래스보다 더 상위(부모 or 조상 클래스)이기에, findPageByAge()의 반환형이
// Page(정확히는 PageRequest)여도 업캐스팅돼서, 받을 수는 있다.
// 내부적으로 limit [ + 1 ]을 하여 1개의 튜플을 추가적으로 더 들고 온다.
Slice<Member> page = repository.findPageByAge(age, pageRequest); // findPageByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌
List<Member> content = page.getContent();
for (Member member : content) {
System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
}
// long totalCount = page.getTotalElements(); Slice에는 이런 기능이 없다.
// System.out.println("totalCount = " + totalCount);
assertThat(content.size()).isEqualTo(3);
//assertThat(page.getTotalElements()).isEqualTo(5); Slice에는 이런 기능이 없다.
assertThat(page.getNumber()).isEqualTo(0);
//assertThat(page.getTotalPages()).isEqualTo(2); Slice에는 이런 기능이 없다.
// 총 2개의 페이지가 반환된다(page 0, page 1)
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
*/
/**
* Slice가 사용되는 예
* -> 모바일 디바이스에서 보면 아래로 쭉 스크롤을 하다가, [더보기]란이 있다.
* 이거의 원리는 처음에 1개의 페이지를 보여주고, Slice가 만약 DB에 1개의 TUPLE이 추가로 남아 있으면 그것을 들고 온다.
* 그렇게 추가의 튜플이 있으면, [더보기]란을 띄워주고, 없으면, 안 띄워 준다.
* -> 만약 Page로 했는데, totalCount가 너무 많으면, Slice 방식을 이용하여 최적화하는 방법도 있다.
*/
repository.save(new Member("member1",10));
repository.save(new Member("member2",10));
repository.save(new Member("member3",10));
repository.save(new Member("member4",10));
repository.save(new Member("member5",10));
int age =10;
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));
// 쿼리 메서드용 SQL문 1번, totalCount용 SQL문 1번 : 총 2번의 쿼리문 날림
// Page는 outer Join을 사용하여 totalCount를 계산한다.
// 그러나 만약 table이 100개 라면 수많은 join이 일어나면서 성능에 문제가 생긴다.
// findPageByAge()를 @Query(countQuERY = "SELECT COUNT() ~~ "으로 JOIN 연산 없이 최적화하였다.
// 실행 결과를 확인해라(게시물에서도 확인 가능)
Page<Member> page = repository.findtotalCountByAge(age, pageRequest); // findByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌
// [도메인 엔티티]는 절대 그대로 클라이언트에게 반환을 하면 안되고, DTO를 만들어서 변환을 해줘야 한다.
// -> 실무의 꿀팁이라며, map()을 사용하여 [paging을 유지하면서] 손쉽게 DTO로 변환하는 법도 있다고 하였다.
Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
// 이젠 Dto로 변환을 했으니, API 결과로서 반환해도 된다.
List<Member> content = page.getContent();
for (Member member : content) {
System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
}
long totalCount = page.getTotalElements(); // totalCount 반환!
System.out.println("totalCount = " + totalCount);
assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
}
@Query(countQuery)를 사용하지 않았을 때!
@Query(countQuery)를 사용하지 했을 때!
'CS 잡지식' 카테고리의 다른 글
SELECT ~~ FOR UPDATE(Feat. Concurrency Problem ) (0) | 2023.05.12 |
---|---|
@EntityGraph(feat. Spring Data JPA가 제공하는 Fetch Join) (0) | 2023.05.12 |
Paging에서의 offset과 limit의 정확한 의미!! (0) | 2023.05.11 |
JPA 스펙 - 반환 타입 List,단건,Optional<단건 엔티티> (0) | 2023.05.11 |
@NamedQuery vs @Query( JPA vs Spring Data JPA ) (0) | 2023.05.11 |