페이징, 정렬
비교를 위해 우선 JPA에서 페이징과 정렬을 사용하는 예제를 보자.
- 검색 조건: 나이가 10살
- 정렬 조건: 이름으로 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지 당 보여줄 데이터는 3건
1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}
스프링 데이터 JPA 페이징, 정렬
- 페이징과 정렬 파라미터
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort가 포함되어 있음)
- 페이징 + 정렬
- 특별한 반환 타입
- org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1 조회)
- List : 추가 count 쿼리 없이 결과만 반환
1
2
3
//repository
Page<Member> findByAge(int age, Pageable pageable);
1
2
3
4
5
6
7
8
9
10
11
12
13
//사용
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
List<Member> content = page.getContent(); //결과 객체들
long totalElements = page.getTotalElements(); //개수
page.getNumber(); //페이지 넘버
page.getTotalPages(); //페이지 개수
page.isFirst();
page.hasNext();
- Page 인터페이스
1
2
3
4
5
public interface Page extends Slice {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
Page map(Function<? super T, ? extends U> converter); //변환기
}
- Slice 인터페이스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Slice extends Streamable {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
Slice map(Function<? super T, ? extends U> converter); //변환기
}
count 쿼리의 분리
1
2
3
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
count를 할 때는 join을 할 필요가 없기때문에 이러한 기능이 생겼다. 만약 분리가 되지 않으면 join으로 인해 예상치 못한 성능 저하가 나타날 수 있다.
단, 하이버네이트 6 이상부터는 의미없는 left join을 자동으로 최적화해주기 때문에 이러한 문제가 없다.
실무 팁
API를 사용할 때 엔티티 그대로를 넘기면 안된다. 문제가 많다.
DTO로 변환해야 한다.
- 페이지를 유지하면서 엔티티를 DTO로 변환
1
2
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
벌크성 수정 쿼리
ex) 모든 직원의 연봉을 10% 인상해라.
- JPA 변경 감지 = 단건
우선 JPA를 사용하는 경우를 보자.
1
2
3
4
5
6
7
public int bulkAgePlus(int age) {
int resultCount = em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
스프링 데이터 JPA의 벌크성 수정 쿼리
1
2
3
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- @Modifying 애노테이션을 사용하면 된다.
- 사용하지 않으면 예외가 발생한다.
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶다면 옵션을 수정하면 된다.
- @Modifying(clearAutomatically = true)
- 기본값은 false이다.
- 이러한 옵션을 신경쓰지 않고 회원을 다시 조회한다면 영속성 컨텍스트에 과거 값이 남아 문제가 발생할 수 있다.
- DB에는 update 되었는데 막상 값을 조회하면 영속성 컨텍스트의 값을 꺼내 매칭이 안되는 문제.
- 다시 조회해야 하는 경우에는 영속성 컨텍스트를 반드시 초기화 하자.
권장 방안
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행
- 영속성 컨텍스트에 엔티티가 있다면 벌크 연산 직후 영속성 컨텍스트 초기화
@EntityGraph
연관된 엔티티들을 SQL 한 번으로 조회하는 방법.
즉 페치조인.
JPQL로 페치조인을 해보자.
1
2
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하도록 도와준다. JPQL 없이 페치 조인을 사용할 수 있다.
스프링 데이터 JPA에서의 페치조인
1
2
3
4
5
6
7
8
9
10
11
12
13
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리 (특히 편리)
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username);
- 사실상 페치 조인의 간편한 버전이라고 보면 된다.
- LEFT OUTER JOIN을 사용.
JPA Hint, Lock
JPA Hint
JPA 쿼리 힌트(SQL 힌트가 아닌 JPA 구현체에게 제공하는 힌트)
- 쿼리 힌트 사용
1
2
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
1
2
3
4
5
6
7
8
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //update 쿼리 실행 X = 변경 감지 X
위의 예제에서는 단순하게 조회용도로 쓰겠다고 선언한 것이다.
Lock
1
2
@Lock(LockModeType.PESSMISTIC_WRITE)
List<Member> findByUsername(String name);