쿼리 코드
// 나이, 시작점, 최대 개수를 지정해서 회원을 조회하고 페이징하는 쿼리 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 ") // 지정한 나이와 같고 이름으로 내림차순 정렬 .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(); }
테스트 코드
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; // 0이면 jpa가 굳이 쿼리에 포함하지 않는다. int limit = 3; //when List<Member> members = memberJpaRepository.findByPage(age, offset, limit); // 페이징 쿼리 long totalCount = memberJpaRepository.totalCount(age); // 전체 회원수 //페이지 계산 공식 적용 // totalPage = totalCount / size => data jpa 기능에서 제공해줌 // 마지막 페이지, 최초 페이지 //then assertThat(members.size()).isEqualTo(3); // limit을 3으로 해서 총 3개의 데이터만 넘어옴 assertThat(totalCount).isEqualTo(5); // 전체 회원수 5명 반환 }
쿼리 코드
Page<Member> findByAge(int age, Pageable pageable);
테스트 코드
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; // page는 0부터 시작 명심하기!, limit은 3, 이름으로 내림차순 정렬 PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); //when // repository에서 Pageable로 받지만 PageRequest 부모 인터페이스에 Pageable이 있다. // 반환 타입이 Page 일 경우에 data JPA가 페이지 계산을 위해서 totalCount 쿼리를 날린다.(전체 데이터수를 알기 위해) // 우리가 정렬 조건을 적용해도 totalCount 쿼리는 정렬이 필요 없으므로 포함되지 않는다. Page<Member> page = memberRepository.findByAge(age, pageRequest); // 페이징 쿼리 //then List<Member> content = page.getContent(); // 페이징 쿼리 결과 데이터 long totalElements = page.getTotalElements(); // totalCount 쿼리 결과 즉, 전체 데이터 개수 assertThat(content.size()).isEqualTo(3); // size를 3으로 설정해서 3개의 데이터 assertThat(page.getTotalElements()).isEqualTo(5); // 전체 회원수는 5명 assertThat(page.getNumber()).isEqualTo(0); // 현재 페이지 번호 assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 개수 assertThat(page.isFirst()).isTrue(); // 첫 페이지인지 assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는지 }
Slice 타입은 totalCount 쿼리를 생성하지 않아서 전체 데이터 수가 많아 Page의 totalCount 쿼리가 성능에 영향을 끼친다면 유용하게 사용할 수 있다.
Slice 타입의 경우에 내가 요청하는 size 보다 +1 조회를 해서 데이터가 있다면 다음 페이지가 있다는 것으로 간주하고 페이지를 미리 로딩하거나 하는 성능 개선 용도로 사용한다(데이터를 3개씩 한 페이지에 보여준다고 가정하고, 4개를 요청했는데 성공하면 3개 이후에 더 보여줄 데이터가 있다는 뜻이므로 다음 페이지를 미리 로딩한다는 뜻)
쿼리 코드
Slice<Member> findByAge(int age, Pageable pageable);
테스트 코드
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; // page는 0부터 시작 명심하기!, limit은 3, 이름으로 내림차순 정렬 PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// page는 0부터 시작 //when // repository에서 Pageable로 받지만 PageRequest 부모 인터페이스에 Pageable이 있다. // 반환 타입이 Page 일 경우에 data JPA가 페이지 계산을 위해서 totalCount 쿼리를 날린다. // 우리가 정렬 조건을 적용해도 totalCount 쿼리는 정렬이 필요 없으므로 포함되지 않는다. // Slice 타입은 size + 1의 결과를 가져온다. Slice<Member> page = memberRepository.findByAge(age, pageRequest); // 페이징 쿼리 //then List<Member> content = page.getContent(); //long totalElements = page.getTotalElements(); // Slice 타입은 전체 데이터 수를 알필요가 없기 때문에 미지원 assertThat(content.size()).isEqualTo(3); // size를 3으로 설정해서 3 + 1개의 데이터 //미지원 assertThat(page.getTotalElements()).isEqualTo(5); // 전체 회원수는 5명 assertThat(page.getNumber()).isEqualTo(0); // 현재 페이지 번호 //미지원 assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 개수 assertThat(page.isFirst()).isTrue(); // 첫 페이지인지 assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는지 }
API 반환을 할 때 엔티티를 외부에 노출시키면 안 되므로 DTO로 매핑을 해주어야 한다.
data JPA는 map 함수로 엔티티에서 DTO로 쉽게 변환하는 기능을 제공한다.Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
Page 타입도 @ResponseBody로 묶어서 반환하게 되면 JSON으로 반환된다.
쿼리 코드
@Query(value = "select m from Member m left join m.team t") Page<Member> findByAge(int age, Pageable pageable);
이런 식으로 페이지에 담을 데이터를 커스텀 할 때 join을 사용하게 되면 data JPA는 totalCount 쿼리를 날릴 때도 join을 사용하게 되어 데이터 수가 많아지면 성능에 영향을 끼치게 된다.
// data JPA가 날린 totalCount 쿼리 select count(member0_.member_id) as col_0_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.team_id
쿼리 코드
@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m) from Member m") Page<Member> findByAge(int age, Pageable pageable);
@Query에 countQuery로 totalCount 쿼리를 따로 지정해주면 불필요한 join 사용을 막아 성능을 개선할 수 있다.
// data JPA가 날린 totalCount 쿼리 select count(member0_.username) as col_0_0_ from member member0_
이전 쿼리문과 다르게 join이 빠진걸 볼 수 있다.