
setFirstResult(offset),setMaxResult(limit)사용
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public List<Member> findByPage(int age, int offset, int limit){
// 나이 10살, 이름 내림차순 조회, 한 페이지당 3건
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();
}
// 총 페이지 구하기
// 성능 최적화를 위해서 sorting 조건 제외
public long totalCount(int age){
return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
.setParameter("age", age)
.getSingleResult();
}
}
@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); // 3개의 데이터 가져옴
assertThat(totalCount).isEqualTo(5); // 총 5명의 멤버 생성했으니, 총 개수는 5
}
참고
JPA는
방언 기반으로 동작해서 DB가 변경되더라도 페이징 쿼리가현재 DB에 맞는 SQL문짜여져서 실행됨
org.springframework.data.domain.Sortorg.springframework.data.domain.Pageable (내부에 Sort 포함)Interface
org.springframework.data 패키지안에 Sort와 Pageable 클래스가 존재한다.
즉, 모든 DB에서 공통의 페이징과 정렬을 사용할 수 있음을 의미한다.
반환 타입에 따라서 totalCount 쿼리 날릴지 결정됨.
1. Page ⭕
2. Slice ❌
3. List ❌
org.springframework.data.domain.Pageorg.springframework.data.domain.SliceList (자바 컬렉션)🚨 주의! 🚨
page는 1이 아닌 0부터 시작
// 페이징과 정렬
Page<Member> findPageByAge(int age, Pageable pageable); // 파라미터 pageable, 반환타입 Page
Slice<Member> findSliceByAge(int age, Pageable pageable); // 파라미터 pageable, 반환타입 Slice
List<Member> findListByAge(int age, Pageable pageable); // 파라미터 pageable, 반환타입 Page
Pageable 인터페이스 구현체를 넘길 때는
PageRequest를 많이 사용
offset,limit,정렬기능 포함 가능
// PageRequest.of(offset, limit, Sort.by(오름차순(내림차순), "기준"));
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
@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"));
// PageRequest(구현체)의 부모를 타고가다보면 Pageable interface가 있음.
// WARN) page 1이 아니라 0부터 시작!!
// when
Page<Member> page = memberRepository.findPageByAge(age, pageRequest);
Slice<Member> slice = memberRepository.findSliceByAge(age, pageRequest);
List<Member> list = memberRepository.findListByAge(age, pageRequest);
// then
// 1) 반환 타입 Page - totalCount쿼리 날라감
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();// 다음 페이지 있는지 여부
// 2. 반환 타입 Slice
// Slice는 total 관련 함수들 지원 X
List<Member> content_slice = slice.getContent();
assertThat(content_slice.size()).isEqualTo(3);
// assertThat(slice.getTotalElements()).isEqualTo(5);
assertThat(slice.getNumber()).isEqualTo(0);
// assertThat(slice.getTotalPages()).isEqualTo(2);
assertThat(slice.isFirst()).isTrue();
assertThat(slice.hasNext()).isTrue();
// 3. 반환 타입 List
// 위의 함수들 동작 안함
// 실무 팁! Entity는 외부로 절대 반환 금지!! DTO로 변환해서 반환!!
Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
// Page유지하면서 DTO로 변경 : api로 반환 가능
// Page 유지하면서 JSON생성될 때, totalPage, totalElement 등이 JSON으로 반환됨
}
반환 타입에 따라서 사용가능한 함수가 다르다!
Slice의 경우 : TotalCount 관련 함수들은 지원하지 않는다.
List의 경우 : 페이징 관련 함수를 모두 지원하지 않는다.
Slice 반환타입의 경우
생성된 쿼리 확인해보면 limit 3이 아닌, limit 4
즉, 내부적으로 limit + 1해서 쿼리를 생성
장점
Page 형식 사용하다가 totalCount 쿼리까지 날라가서 성능이 안나오니까 Slice 형식으로 변경하자! 했을 때,
Spring Data JPA를 사용하면반환 타입만 변경하면 해결!
Page만 사용가능
getTotalPages() : 총 페이지 수getTotalElements() : 전체 개수Page, Slice 모두 사용 가능
getNumber() : 현재 페이지 번호getSize() : 페이지 당 데이터 개수getNumberOfElements() : 페이지 당 실제 데이터 개수hasNext(): 다음 페이지 존재 여부hasPrevious(): 이전 페이지 존재 여부isFirst(): 시작 페이지 여부getContent(): 실제 컨텐츠 가져오는 메서드. (반환 타입 : List<Entity>)get(): 실제 컨텐츠 가져오는 메서드. (반환 타입 : Stream<Entity>)getSize vs getNumberOfElements
repository.findAll(new PageRequest(0, 30));일 때, 30개의 데이터를 요청했지만, 실제로는 10개의 데이터만 들어있다고 가정하자.
getSize() = 30
getNumberOfElements() = 10
다대일 left outer join을 할 때, 데이터를 가져올 때는 join을 해야하지만, totalCount 쿼리에서는 join을 하지 않아도 count 값은 같다.
데이터를 가져오는 쿼리가 복잡하면, count 쿼리도 복잡해져서 성능 최적화가 필요할 때가 있다.
@Query(value = "데이터 가져오는 쿼리", countQuery = "count 쿼리")
// totalCount 성능 최적화를 위한, 쿼리 분리(값 가져오는 쿼리, totalCount 쿼리)
@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllBy(Pageable pageable); // 파라미터 pageable, 반환타입 Page
참고
Sorting도 복잡해지면 PageRequest안에서 해결하지 말고, 직접 @Query에 Sorting 쿼리를 적어주는 것이 편하다!