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.Sort
org.springframework.data.domain.Pageable
(내부에 Sort 포함)Interface
org.springframework.data
패키지안에 Sort와 Pageable 클래스가 존재한다.
즉, 모든 DB에서 공통의 페이징과 정렬을 사용할 수 있음을 의미한다.
반환 타입에 따라서 totalCount 쿼리 날릴지 결정됨.
1. Page ⭕
2. Slice ❌
3. List ❌
org.springframework.data.domain.Page
org.springframework.data.domain.Slice
List
(자바 컬렉션)🚨 주의! 🚨
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 쿼리를 적어주는 것이 편하다!