순수 JPA 페이징과 정렬
JPA에서 페이징을 어떻게 할 것인가?
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자
검색 조건 : 나이가 10살
정렬 조건 : 이름으로 내림차순
페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
아래는 순수 JPA 페이징으로 정렬
MemberJpaRepository.java
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();
}
MemberJpaRepositoryTest.java
@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);
// 페이지 계산 공식 적용
// totalPage = totalCount / size ...
// 마지막 페이지 ...
// 최초 페이지 ...
Assertions.assertThat(members.size()).isEqualTo(3);
Assertions.assertThat(totalCount).isEqualTo(5);
}
스프링 데이터 JPA에 최초 페이지, 마지막 페이지 등 페이지 계산 공식 같은 것 이미 기능으로 다 제공하고 있다.
스프링 데이터 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 쿼리 없이 결과만 반환
아래처럼 @Query 사용하여 페이징 쿼리와 카운팅 쿼리 분리할 수 있다.
MemberRepository.java
@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);
MemberRepositoryTest.java
@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"));
// when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> toMap =
page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
// then
List<Member> content = page.getContent();
long totalElements = page.getTotalElements();
Assertions.assertThat(content.size()).isEqualTo(3);
Assertions.assertThat(page.getTotalElements()).isEqualTo(5);
Assertions.assertThat(page.getNumber()).isEqualTo(0);
Assertions.assertThat(page.getTotalPages()).isEqualTo(2);
Assertions.assertThat(page.isFirst()).isTrue();
Assertions.assertThat(page.hasNext()).isTrue();
}
벌크성 수정 쿼리
순수 JPA의 벌크성 수정 쿼리
MemberJpaRepository.java
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;
}
MemberJpaRepositoryTest.java
@Test
public void bulkUpdate(){
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 19));
memberJpaRepository.save(new Member("member3", 20));
memberJpaRepository.save(new Member("member4", 21));
memberJpaRepository.save(new Member("member5", 40));
// when
int resultCount = memberJpaRepository.bulkAgePlus(20);
// then
Assertions.assertThat(resultCount).isEqualTo(3);
}
스프링 Data Jpa 레퍼지토리
JPA에서 수정하는 쿼리의 경우 @Modifying(clearAutomatically = true) 어노테이션이 필수이다.
그래야 순수 JPA에서 .executeUpdate() 메서드가 실행되게 된다.
아래는 JPA에서 벌크 업데이트 로직 수행할 때 주의사항이다.
List<Member> result = memberRepository.findByUsername("member5");
Member member5 = result.get(0);
// 벌크 update 연산은 영속성 컨텍스트 안 거치고 바로 DB에 반영하기에, 컨텍스트에는 40살, DB에는 41살로 되어 있다.
// 따라서 위에 em.flush, em.clear 하여 JPA 영속성 컨텍스트에 있는 캐시된 데이터들을 날리고, DB에서 읽어오도록 해야 한다.
// 아니면 @Modifying(clearAutomatically = true) 설정해도 된다.
System.out.println("member5 = " + member5);
MemberRepository.java
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
MemberRepositoryTest.java
@Test
public void bulkUpdate(){
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
em.flush();
em.clear();
// List<Member> result = memberRepository.findByUsername("member5");
// Member member5 = result.get(0);
// // 벌크 update 연산은 영속성 컨텍스트 안 거치고 바로 DB에 반영하기에, 컨텍스트에는 40살, DB에는 41살로 되어 있다.
// // 따라서 위에 em.flush, em.clear 하여 JPA 영속성 컨텍스트에 있는 캐시된 데이터들을 날리고, DB에서 읽어오도록 해야 한다.
// // 아니면 @Modifying(clearAutomatically = true) 설정해도 된다.
// System.out.println("member5 = " + member5);
// then
Assertions.assertThat(resultCount).isEqualTo(3);
}
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법
JPA N+1 문제를 해결하기 위한 방법으로 JPQL에서 fetch join이 있다.
이 방법은 jpql을 직접 작성하는 경우에 사용할 수 있고,
jpql을 작성하지 않는 경우는 아래처럼 @EntityGraph(attributePaths = {"필드명"}) 으로 가짜(프록시) 객체가 아니라 연관된 엔티티를 바로 가져올 수 있다.
MemberRepository.java
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);
Member.java
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
JPA Hint & Lock
JPA 영속성 컨텍스트에 엔티티를 가져오면 추후 수정 시 변경 감지를 위하여 복사본이 생성되는데, 그 엔티티의 값을 수정할 필요가 없는 경우는 복사본 생성은 불필요한 리소스 낭비가 된다.
따라서 그런 readOnly 쿼리의 경우 아래처럼 값을 주면 엔티티의 값을 복사하지 않아서 나름의 최적화를 할 수 있다.
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value="true"))
하지만 성능에 매우 큰 영향을 주는 요소는 아니여서, 모든 readOnly 쿼리 메서드에 일일이 붙일 필요는 없다고 한다.
MemberRepository.java
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value="true"))
Member findReadOnlyByUsername(String username);
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
MemberRepositoryTest.java
@Test
public void queryHint(){
// given
Member member1 = new Member("member1", 10);
memberRepository.save(member1);
em.flush();
em.clear();
// when
Member findMember = memberRepository.findReadOnlyByUsername("member1");
findMember.setUsername("member2");
em.flush();
}
@Test
public void lock(){
// given
Member member1 = new Member("member1", 10);
memberRepository.save(member1);
em.flush();
em.clear();
// when
List<Member> result = memberRepository.findLockByUsername("member1");
}
'JPA' 카테고리의 다른 글
섹션 6. 확장기능 (0) | 2024.09.30 |
---|---|
실전! 스프링 데이터 JPA / 섹션 4. 공통 인터페이스 기능 (0) | 2024.09.27 |
실전! 스프링 데이터 JPA / 3. 예제 도메인 모델 (0) | 2024.09.26 |
최범균 Spring Data JPA 기초 (0) | 2024.09.25 |
@Transactional 어노테이션 관련 인상깊게 정보 본 블로그 (0) | 2023.08.29 |
댓글