JPA 페이징
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", Member.class)
.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();
}
@Test
void paging() throws Exception {
//given
memberJpaRepository.save(new Member("1번", 10));
memberJpaRepository.save(new Member("2번", 10));
memberJpaRepository.save(new Member("3번", 10));
memberJpaRepository.save(new Member("4번", 10));
memberJpaRepository.save(new Member("5번", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
//then
for (Member member : members) {
System.out.println("member = " + member);
}
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
/*
member = Member(id=5, username=5번, age=10)
member = Member(id=4, username=4번, age=10)
member = Member(id=3, username=3번, age=10)
*/
limit는 있는데 왜 offset은 없나요?
JPA는 page가 0부터 시작하는데, 내부적으로 "최적화"가 되어 있어 offset이 0이면 쿼리에 보이지 않는다
만약에 offset이 1이라면 쿼리에 보일 것이다. 아래 쿼리는 offset을 1로 바꿨을 때이다
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_ from member member0_ where member0_.age=10 order by member0_.username desc limit 3 offset 1;
totalCount는 왜 필요한가요?
totalCount는 데이터의 개수를 의미한다.
즉, 총 페이지 개수나 몇 번째 페이지 를 알기위해 totalCount가 필요하다.
spring data JPA 페이징
spring data에서 페이징과 정렬을 제공하기 위해 Pageable, Sort 를 제공한다
- 파라미터에 "Pageable"를 사용하면 반환타입을 Page, Slice 또는 List를 사용할 수 있다
- Page를 사용하면 spring data jpa는 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다
- Slice를 사용하면 Page와 다르게 count 쿼리 결과를 포함하지 않고, 다음 페이지만 확인할 수 있음
- 내부적으로 limit + 1 조회함
아래의 그림은 org.springframework.data.domain의 클래스 다이어그램을 일부이다
빨간색 상자는 앞으로 우리가 사용할 클래스나 인터페이스라고 생각하면 된다
Page
Page 객체는 어떻게 생성하나요?
Page<Member> findByAge(int age, Pageable pageable);
Pageable을 파라미터로 사용하는데, 쿼리에 대한 조건들이 들어가있다고 생각하면 된다
Pageable을 구현하는 PageRequest 객체를 생성하여 파라미터로 넣어준다
@Test
void paging() throws Exception {
//given
memberRepository.save(new Member("1번", 10));
memberRepository.save(new Member("2번", 10));
memberRepository.save(new Member("3번", 10));
memberRepository.save(new Member("4번", 10));
memberRepository.save(new Member("5번", 10));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
//when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
//then
List<Member> content = page.getContent();
for (Member member : content) {
System.out.println("member = " + member);
}
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();
}
- page
- 출력할 페이지를 나타냄
- size
- 데이터의 개수
- sort
- 정렬 조건
- 앞서 말했듯이 Page를 사용하면 조회된 게시글 수를 count하는 쿼리가 나가는 것을 알 수 있다
- Page와 Slice 인터페이스에서 제공하는 다양한 메서드를 통해 페이징 기능을 사용할 수 있다
Page 객체를 생성할 때 첫번째 파라미터인 page가 무엇인가요?
페이징을 했을 때, 지정된 page에 존재하는 데이터를 가져올 수 있는 파라미터이다
즉, 페이징을 하고 나서 특정 page에 존재하는 데이터를 가져올 수 있다
아래의 코드를 보면 1~9번까지의 member가 있다
이름을 기준으로 내림차순을 하게 되면 9,8,7 / 6,5,4 / 3,2,1 로 페이지가 구성이 된다
지정된 page 파라미터에 따라 조회되는 데이터가 달라짐을 알 수 있다
@Test
void paging() throws Exception {
//given
memberRepository.save(new Member("1번", 10));
memberRepository.save(new Member("2번", 10));
memberRepository.save(new Member("3번", 10));
memberRepository.save(new Member("4번", 10));
memberRepository.save(new Member("5번", 10));
memberRepository.save(new Member("6번", 10));
memberRepository.save(new Member("7번", 10));
memberRepository.save(new Member("8번", 10));
memberRepository.save(new Member("9번", 10));
int age = 10;
PageRequest pageRequest1 = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
PageRequest pageRequest2 = PageRequest.of(1, 3, Sort.by(Sort.Direction.DESC, "username"));
PageRequest pageRequest3 = PageRequest.of(2, 3, Sort.by(Sort.Direction.DESC, "username"));
//when
Page<Member> page1 = memberRepository.findByAge(age, pageRequest1);
Page<Member> page2 = memberRepository.findByAge(age, pageRequest2);
Page<Member> page3 = memberRepository.findByAge(age, pageRequest3);
//then
List<Member> content1 = page1.getContent();
List<Member> content2 = page2.getContent();
List<Member> content3 = page3.getContent();
System.out.println("첫번째 페이지");
for (Member member : content1) {
System.out.println("member = " + member);
}
System.out.println("두번째 페이지");
for (Member member : content2) {
System.out.println("member = " + member);
}
System.out.println("세번째 페이지");
for (Member member : content3) {
System.out.println("member = " + member);
}
}
/*
첫번째 페이지
member = Member(id=9, username=9번, age=10)
member = Member(id=8, username=8번, age=10)
member = Member(id=7, username=7번, age=10)
두번째 페이지
member = Member(id=6, username=6번, age=10)
member = Member(id=5, username=5번, age=10)
member = Member(id=4, username=4번, age=10)
세번째 페이지
member = Member(id=3, username=3번, age=10)
member = Member(id=2, username=2번, age=10)
member = Member(id=1, username=1번, age=10)
*/
Slice
- Page와 다르게 count 쿼리 없이 다음 페이지만을 확인한다
- limit + 1 만큼을 조회한다
- count 쿼리가 없기 때문에 Page에서 사용할 수 있었던 "getTotalPages", "getTotalElements" 메서드를 사용할 수 없다
Slice<Member> findByAge(int age, Pageable pageable);
@Test
void slice() throws Exception {
//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"));
//when
Slice<Member> page = memberRepository.findByAge(age, pageRequest);
//then
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();
}
PageRequest를 생성할 때 size를 3으로 지정했는데 limit 4 쿼리가 나간 것을 알 수 있다
- Slice는 페이지가 총 몇개인지는 모르겠지만, 다음 페이지가 있어 없어 이 정도로 확인한다
- 그래서 몇 번째 페이지까지 존재하느냐는 모르지만, 우리가 자주 사용하는 모바일 애플리케이션에서 "더보기"를 사용할 때 Slice를 사용할 수 있음
List
List<Member> findByAge(int age, Pageable pageable);
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
//when
List<Member> page = memberRepository.findByAge(age, pageRequest);
Page, Slice 뿐만 아니라 List로도 반환할 수 있다
실무에서의 페이징
join을 이용하여 Page를 구현할 경우 count도 같이 join을 한다?
Page를 잘 안쓰려고 하는 이유는 count 쿼리가 나가게 된다
- 즉, 예시를 보면 left outer join을 사용하게 되면 count도 left outer join을 사용함
- left outer join을 사용할 경우 count도 굳이 left outer join할 필요가 없다
- 따라서 복잡한 sql일수록 조인이 많이 일어나고, count도 조인을 같이 하게 되면 성능이 매우 안좋아짐
- count 쿼리를 분리하여 해결할 수 있음
count 쿼리를 분리하지 않을 때
@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
count 쿼리를 분리하고 난 후
@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);
페이지를 유지하며 Entity -> Dto 변환
우리는 앞서 Entity 자체를 반환하면 안되고 Dto로 변환하여 반환해야한다고 했다
Page에서는 내부적으로 map 메서드를 통해 Function 인터페이스를 인자로 받아 변환해줄 수 있다
<U> Page<U> map(Function<? super T, ? extends U> converter);
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), member.getTeam().getName()));
REFERENCES
'Spring 강의 > Spring data JPA' 카테고리의 다른 글
@EntityGraph (0) | 2022.04.13 |
---|---|
벌크 연산 (0) | 2022.04.12 |
쿼리 메서드 (0) | 2022.04.08 |