반환 타입
조회시 다양한 반환타입을 원할 수 있다. 만약 특정 회원의 username으로 조회시 username이 고유하다면 하나의 회원만 조회하고 싶을 것이고, 고유하지 않다면 List로 받고 싶을 수도 있다. 이럴 때는 아래와 같이 메서드 반환타입에 원하는 반환타입을 적어주면 data jpa가 알아서 잘 쿼리를 날려준다.
아래의 Repository를 보자.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import study.datajpa.entity.Member;
import study.datajpa.entity.dto.MemberDto;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
...
//컬렉션 조회 : List<Member> 반환타입 명시
List<Member> findListByUsername(String username); /*리스트는 절대 Null아님!*/
//단건 조회 : Member 반환타입 명시
Member findMemberByUsername(String username);
//단건 Optional 조회 : Optioanl<Member> 반환타입 명시
Optional<Member> findOptionalByUsername(String username);
...
}
참고로 컬렉션 조회는 조회된 결과가 없더라도 null이 아닌 빈 컬렉션을 반환해준다.
이를 테스트해보자.
@Test
void findMember(){
Member member1 = new Member("member1", 10, null);
Member member2 = new Member("member2", 10, null);
memberRepository.save(member1);
memberRepository.save(member2);
List<Member> members = memberRepository.findListByUsername("member1");
Member findMember = memberRepository.findMemberByUsername("member1");
Optional<Member> optionalMember = memberRepository.findOptionalByUsername("member1");
for (Member member : members) System.out.println(member);
System.out.println(findMember);
System.out.println(optionalMember);
}
실행결과:
Member(id=1, username=member1, age=10)
Member(id=1, username=member1, age=10)
Optional[Member(id=1, username=member1, age=10)]
잘 나오는 것을 볼 수 있다.
조회 결과가 많거나 없으면?
컬렉션의 경우에는 결과가 없다면 빈 컬렉션 반환
단건 조회의 경우에는 결과가 없다면 null반환, 결과가 2건 이상이라면 javax.persistence.NonUniqueResultException 예외 발생
참고: 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다. 이 메서드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 하여 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환한다.
페이징
순수 JPA 페이징과 정렬
먼저 순수하게 JPA만을 사용한 페이징을 보자. 해당 예시는 특정 나이의 Member들을 조회하는 코드이다.
@Repository
public class MemberJPArepository {
@PersistenceContext
private EntityManager em;
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 pagingByAgeTest(){
int age = 20; //age가 20인 member 조회
int offset = 0;//맨 처음 부터 (JPA에서 맨 처음 페이지는 1이 아닌 0부터 이다.)
int limit = 3; //3개의 데이터 조회
//10살인 멤버 1명
Member member10 = new Member("member", 10, null);
memberJPArepository.save(member10);
//20살인 멤버 30명
for(int i=0; i<30; i++){
Member member = new Member("member"+i, 20, null);
memberJPArepository.save(member);
}
long numOf20Member = memberJPArepository.totalCount(age);
assertThat(numOf20Member).isEqualTo(30L);//20살인 멤버 30명 맞나?
List<Member> members = memberJPArepository.findByPage(age, offset, limit);
assertThat(members.size()).isEqualTo(limit);//가져온 페이징 목록에 limit(3명)수 만큼의 멤버?
}
테스트는 성공했다.
data JPA의 페이징
하지만 data JPA를 사용하면 더 쉬운 페이징을 제공한다.
아래와 같이 data jpa Repository에 작성해주면 되는 Page<T> 를 반환타입으로 하면 된다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import study.datajpa.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);//Page<Member>타입을 반환
}
우리가 원하는 조건인 age와, Pageable인터페이스를 인자로 넣어준다.
아래의 PageRequest.of 메서드는 원하는 Page관련 정보를 담을 수 있다. 이를 테스트해보자. (PageRequest는 Pageable 타입임)
@Test
void pagingByAgeTest(){
memberRepository.save(new Member("member1", 10, null));
memberRepository.save(new Member("member2", 10, null));
memberRepository.save(new Member("member3", 10, null));
memberRepository.save(new Member("member4", 10, null));
memberRepository.save(new Member("member5", 10, null));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "username");
Page<Member> page = memberRepository.findByAge(age, pageRequest);//totalCount까지 같이 날린다.
//조회한 member들
List<Member> members = page.getContent();
for (Member member : members) System.out.println(member);
//총 데이터 개수
long totalElements = page.getTotalElements();
System.out.println("totalElements = " + totalElements);
//현재 페이지
int number = page.getNumber();
System.out.println("number = " + number);
//총 페이지
int totalPages = page.getTotalPages();
System.out.println("totalPages = " + totalPages);
//현재 첫 페이지인가?
boolean isFirst = page.isFirst();
System.out.println("isFirst = " + isFirst);
//다음 페이지가 있는가?
boolean hasNext = page.hasNext();
System.out.println("hasNext = " + hasNext);
}
PageRequest.of(0, 3, Sort.Direction.DESC, "username");
위의 PageRequest.of를 살펴보면 PageRequest.of(시작 페이지, 몇 개를 가져올지, 정렬 방법, 정렬 조건);
테스트 결과 아래와 같은 쿼리가 나간다.
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=?
order by
member0_.username desc limit ?
// count쿼리까지 날려준다.
select
count(member0_.member_id) as col_0_0_
from
member member0_
where
member0_.age=?
출력문 결과:
Member(id=5, username=member5, age=10)
Member(id=4, username=member4, age=10)
Member(id=3, username=member3, age=10)
totalElements = 5 //총 Member의 수
number = 0 //현재 페이지
totalPages = 2 //총 페이지 개수
isFirst = true //현재 첫 페이지 인가?
hasNext = true //다음 페이지가 있나?
결과를 보면 username을 기준으로 역순으로 잘 출력된 것을 볼 수 있다. 또한 totalElements도 5개로 잘 가져온 것을 볼 수 있다.
심지어 현재 몇 페이지인지와 총 페이지 개수까지 알려준다!!! (jpa에서 페이지는 0부터 시작)
Slice
스마트 폰을 사용하여 게시판을 보다 보면 아래로 슬라이드를 하면서 다음 게시글을 볼 수 있다. 이때 사용할 수 있는 Slice를 사용해보자.
아래와 같이 반환타입을 Page에서 Slice로 바꿔줘야한다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import study.datajpa.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {
// Page<Member> findByAge(int age, Pageable pageable);
Slice<Member> findByAge(int age, Pageable pageable); //Slice 사용!
}
이를 테스트해보자. (Slice에서는 위의 Page에서 사용한 getTotalPages, getTotalElement 는 제공하지 않는다.)
@Test
void pagingSliceByAgeTest(){
memberRepository.save(new Member("member1", 10, null));
memberRepository.save(new Member("member2", 10, null));
memberRepository.save(new Member("member3", 10, null));
memberRepository.save(new Member("member4", 10, null));
memberRepository.save(new Member("member5", 10, null));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "username");
Slice<Member> slice = memberRepository.findByAge(age, pageRequest);//totalCount까지 같이 날린다.
//조회한 member들
List<Member> members = slice.getContent();
for (Member member : members) System.out.println(member);
//현재 페이지
int number = slice.getNumber();
System.out.println("number = " + number);
//현재 첫 페이지인가?
boolean isFirst = slice.isFirst();
System.out.println("isFirst = " + isFirst);
//다음 페이지가 있는가?
boolean hasNext = slice.hasNext();
System.out.println("hasNext = " + hasNext);
}
테스트 결과:
Member(id=5, username=member5, age=10)
Member(id=4, username=member4, age=10)
Member(id=3, username=member3, age=10)
number = 0
isFirst = true
hasNext = true
Page와 Count쿼리
Page인터페이스를 사용하면 Count쿼리까지 날려주는 것을 보았다. 하지만 만약 특정 조회 sql에 join이 있다고 해보자, 이때 count쿼리도 마찬가지로 join을 한다. 이를 테스트해보자.
@Test
void pagingCountQueryTest(){
memberRepository.save(new Member("member1", 10, null));
memberRepository.save(new Member("member2", 10, null));
memberRepository.save(new Member("member3", 10, null));
memberRepository.save(new Member("member4", 10, null));
memberRepository.save(new Member("member5", 10, null));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "username");
Page<Member> page = memberRepository.findByAge(age, pageRequest);//totalCount까지 같이 날린다.
//조회한 member들
List<Member> members = page.getContent();
for (Member member : members) System.out.println(member);
}
위를 실행해보면 아래와 같이 두개의 쿼리가 나간다.
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_
left outer join
team team1_
on member0_.team_id=team1_.team_id
order by
member0_.username desc limit ?
// count쿼리도 join을 하고 있다.
select
count(member0_.member_id) as col_0_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
하지만 굳이? count쿼리에서도 join을 하는 것은 성능 저하의 원인이 될 수 있다. 하여 이 둘을 따로 분리할 수 있는 방법을 제공한다.
아래와 같다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select m from Member m left join m.team", countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
}
이를 다시 테스트 해보면 아래와 같다.
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_
left outer join
team team1_
on member0_.team_id=team1_.team_id
order by
member0_.username desc limit ?
// count쿼리에 Join이 사라지고 우리가 지정한 countQuery가 나갔다
select
count(member0_.username) as col_0_0_
from
member member0_
DTO로 쉽게 변환하기
Pageable은 map을 제공한다. 아래처럼 쉽게 DTO로 변경할 수 있다.
@Test
void pagingMemberToDTO(){
memberRepository.save(new Member("member1", 10, null));
memberRepository.save(new Member("member2", 10, null));
memberRepository.save(new Member("member3", 10, null));
memberRepository.save(new Member("member4", 10, null));
memberRepository.save(new Member("member5", 10, null));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "username");
Page<Member> page = memberRepository.findByAge(age, pageRequest);//totalCount까지 같이 날린다.
// DTO로 쉽게 변환하기
Page<MemberDto> tpMap = page.map((m) ->
new MemberDto(m.getId(), m.getUsername(), m.getTeam().getName()));
}
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'JPA > 스프링 DATA JPA' 카테고리의 다른 글
Data JPA 08 @EntityGraph (0) | 2022.01.07 |
---|---|
Data JPA 07 벌크성 수정 쿼리 (0) | 2022.01.07 |
Data JPA 05 쿼리 메서드 - Query, 리포지토리 메서드에 쿼리 정의하기 (0) | 2022.01.05 |
Data JPA 04 쿼리 메서드 - NamedQuery (0) | 2022.01.05 |
Data JPA 03 쿼리 메소드 - 메서드 이름으로 쿼리 생성 (0) | 2022.01.05 |