Spring Data Jpa의 특징을 차례로 알아보자.
1. 메소드 이름으로 쿼리 생성
*메소드 이름
조회 : find...By + where 문에 들어갈 조건
개수 : count...By
존재 : exist...By
삭제 : delete...By
DISTINCT : findDistinct
LIMIT : findFirst, findFirst3, findTop, findTop3
cf. Entity 필드명이 변경되면, Spring Data Jpa 인터페이스에 정의한 메서드 이름도 변경해야 한다.
애플리케이션 로딩 시점에, 메서드를 parsing 해서 sql 쿼리문을 생성하기 때문에
필드명이 변경되면 parsing 시점에서 문법 오류를 던진다.
[순수 JPA]
public List<Member> findByUserNameAndAgeGreaterThan(String username, int age) {
return em.createQuery("select m From Member m where m.username = :username and m.age > :age")
.setParameter("username", username)
.setParameter("age", age)
.getResultList();
}
[스프링 데이터 JPA]
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
2. @Query
규칙 + Entity 필드명으로 메소드 이름을 짓는 것 대신에
메소드 이름은 자유롭게 짓고, @Query() 내에 jpql 쿼리를 직접 작성함으로써 원하는 결과를 받을 수 있다.
[스프링 데이터 JPA]
@Query("select m from Member m where m.username = :username and m.age > :age")
List<Member> findMember(@Param("username") String username, @Param("age") int age);
3. DTO 조회
[순수 JPA]
public List<MemberDto> findMemberDto() {
return em.createQuery("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
.getResultList()
}
[스프링 데이터 JPA]
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
4. 파라미터 바인딩
[순수 JPA]
public List<Member> findByNames(Collection<String> names) {
return em.createQuery("select m from Member m where m.username in :names")
.setParameter("names", names)
.getResultList();
}
[스프링 데이터 JPA]
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
5. 반환 타입
Spring Data Jpa는 유연한 반환 타입을 지원한다.
조회 결과가 많거나 없으면??
컬렉션
- 결과 없음 : 빈 컬렉션
단일 조회
- 결과 없음 : null
- 2건 이상 : javax.persistence.NonUniqueResultException`예외 발생
[순수 JPA]
// 1) 컬렉션
public List<Member> findListByUserName(String username) {
return em.createQuery("select m from Member m where m.username = :username")
.setParameter("username", username)
.getResultList();
}
// 2-1) 단일
public Member findMemberByUserName(String username) {
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getSingleResult();
}
// 2-2) Optional
public Optional<Member> findOptionalByUserName(String username) {
Member member = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getSingleResult();
return Optional.ofNullable(member);
}
[스프링 데이터 JPA]
List<Member> findListByUsername(String username); // 컬렉션
Member findMemberByUsername(String username); // 단일
Optional<Member> findOptionalByUsername(String username); // Optional
6. 페이징과 정렬
* Page는 0부터 시작이다.
페이징, 정렬 파라미터
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Page : 페이징 기능 (내부에 Sort 포함)
반환 타입
- org.springframework.data.domain.Page : 카운트 쿼리를 포함하는 페이징
- org.springframework.data.domain.Slice : 카운트 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1 조회)
- List<> : 카운트 쿼리 없이 결과만 반환
[순수 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 ")
.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]
⭐️ count 쿼리를 분리
- 전체 count 쿼리는 연관 관계의 테이블을 left join 하므로, 매우 무겁다
따라서, count 쿼리를 주테이블만 조회하게끔 별도로 작성하여 left join을 피해야 한다.
- 스프링 부트 3(하이버네이트 6)부터 left join을, 다음 실행 결과와 같이 최적화 한다.
Hibernate:
select
count(m1_0.member_id)
from
member m1_0
@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);
//Slice<Member> findByAge(int age, Pageable pageable); // 카운트 쿼리를 보내지 않음
+. PageRequest는 Pageable interface의 구현체이다.
PageRequest(pageNumber, pageSize, sort) → 현재 페이지, 조회할 데이터 수 , 정렬 정보
@Test
public void paging() {
//given
memberRepository.save(new Member("member1", 20));
memberRepository.save(new Member("member2", 20));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); // sorting 조건은 선택
Page<Member> page = memberRepository.findByAge(20, pageRequest); // page 계산을 위해 count 쿼리까지 같이 보낸다.
// DTO 변환
Page<MemberDto> mapDto = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
//then
List<MemberDto> pageContent = mapDto.getContent(); // 조회 데이터
assertThat(pageContent.size()).isEqualTo(3); // 조회 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수(Slice x)
assertThat(page.getNumber()).isEqualTo(0); // 페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 번호(Slice x)
assertThat(page.isFirst()).isTrue(); // 첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는가?
}
7. 벌크성 수정 쿼리
[순수 JPA]
public int bulkAgePlus(int age) {
return em.createQuery("update Member m set m.age = m.age + 1 " +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
}
[스프링 데이터 JPA]
⭐️ 벌크 연산은 영속성 컨텍스트를 무시하고 실행한다.
- 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
- 따라서 벌크연산 이후, '@Modifying(clearAutomatically=true)' 로 영속성 컨텍스트를 초기화하자!!
- 사용하지 않으면 다음의 예외가 발생
`org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
// 쿼리는 select로 나가는 것이 아니라 해당 어노테이션이 있어야 executeUpdate()가 호출된다.
// 그렇지 않으면 getSingleList(), getResultList()를 호출해버린다.
@Modifying (clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
8. @EntityGraph
- 간단하게 join fetch를 사용하는 경우
- 기존 Spring Data Jpa처럼 jpql을 쓰지 않고 메서드 이름으로 쿼리를 만들고 싶을 때 활용
- Left Outer Join을 사용
- 내부적으로 join fetch를 사용함
[스프링 데이터 JPA]
// 1. join fetch
@Query("select m from Member m join fetch m.team")
List<Member> findMemberFetchJoin();
// 2. EntityGraph
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll(); // JPQL X
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph(); // JPQL + Entity Graph
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphAllByUsername(@Param("username") String username); // 메서드 이름으로 쿼리에서 편리
9. JPA 쿼리 힌트 & Lock
JPA 쿼리 힌트
→ SQL 힌트가 아니라, JPA 구현체(hibernate)에게 제공하는 힌트이다.
JPA 제공 락
→ 동시성을 관리하기 위해 여러 종류의 Lock을 제공한다.
[스프링 데이터 JPA]
// Query Hint 예시
// 변경 감지를 위한 snapshot을 만들지 않는다. (최적화)
// 대용량 데이터이지 않는 한 성능 향상이 미미하다. (이 경우에도 redis 사용)
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
// select for update, 비관적 락
@Lock(LockModeType.PESSIMISTIC_WRITE)
Member findLockByUsername(String username);
쿼리 힌트 Test.
- 쿼리의 반환값에 대해 'readOnly'로 설정하였으므로, 해당 쿼리로 불러온 Member는 변경되지 않는다.
@Test
public void queryHint() {
// given
Member member = new Member("member1", 10);
memberRepository.save(member);
em.flush();
em.clear();
// when
Member findMember = memberRepository.findReadOnlyByUsername("member1");
findMember.setUsername("member2"); // 변경감지 X, update 쿼리 안 나간다.
em.flush();
}
'Spring > Spring Data Jpa' 카테고리의 다른 글
6. 스프링 데이터 JPA 분석 (0) | 2024.04.12 |
---|---|
5. 확장 기능 (0) | 2024.04.11 |
3. 공통 인터페이스 기능 (1) | 2024.03.24 |
2. 예제 도메인 모델 (1) | 2024.03.23 |
1. 프로젝트 환경설정 (0) | 2024.03.22 |