[스프링 데이터 JPA] 4. 쿼리 메소드 기능
📌 쿼리 메소드 기능 3가지
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
@Query
어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의
📌 메소드 이름으로 쿼리 생성
- 메소드 이름을 분석해서 JPQL 쿼리 실행
📌 순수 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 리포지토리 테스트 코드
@Test
public void findByUsernameAndAgeGreaterThan(){
Member m1 = new Member("AAA", 10);
Member m2 = new Member("AAA", 20);
memberJpaRepository.save(m1);
memberJpaRepository.save(m2);
List<Member> result = memberJpaRepository.findByUsernameAndAgeGreaterThan("AAA", 15);
assertThat(result.get(0).getUsername()).isEqualTo("AAA");
assertThat(result.get(0).getAge()).isEqualTo(20);
assertThat(result.size()).isEqualTo(1);
}
📌 스프링 데이터 JPA
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
- 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 조회: find...By ,read...By ,query...By get...By,
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
- ex) findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 됨
- COUNT: count...By 반환타입
long
- EXISTS: exists...By 반환타입
boolean
- 삭제: delete...By, remove...By 반환타입
long
- DISTINCT: findDistinct, findMemberDistinctBy
- LIMIT: findFirst3, findFirst, findTop, findTop3
cf) 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 함
➡ 그렇지 않으면 애플리케이션을 시작하는 시점에 오류 발생
➡ 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
📌 JPA NamedQuery (잘 사용 X)
@NamedQuery(
name="Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {...}
➡ 엔티티에 넣어놓고
JPA를 직접 사용해서 Named 쿼리 호출
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
➡ 불러올 수 있음
스프링 데이터 JPA로 NamedQuery 사용
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
@Query
를 생략하고 메서드 이름만으로 Named 쿼리를 호출 가능
스프링 데이터 JPA로 Named 쿼리 호출
public interface MemberRepository extends JpaRepository<Member, Long> { //** 여기 선언한 Member 도메인 클래스
List<Member> findByUsername(@Param("username") String username);
}
- 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
- 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략 사용
- 필요하면 전략을 변경할 수 있지만 권장하지 않음
cf)스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
➡ 대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
📌 @Query, 리포지토리 메소드에 쿼리 정의하기
메서드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int age);
}
@org.springframework.data.jpa.repository.Query
어노테이션을 사용- 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
cf) 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해짐
➡ @Query 기능 자주 사용
📌 @Query, 값, DTO 조회하기
단순히 값 하나를 조회
@Query("select m.username from Member m")
List<String> findUsernameList();
- JPA 값 타입(
@Embedded
)도 이 방식으로 조회 가능
DTO 생성
package study.datajpa.dto;
import lombok.Data;
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
}
DTO로 직접 조회
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
❗ 주의
DTO로 직접 조회 하려면 JPA의 new
명령어를 사용해야 함
➡ 생성자가 맞는 DTO가 필요
➡ JPA와 사용방식이 동일
📌 파라미터 바인딩
- 위치 기반
- 이름 기반
select m from Member m where m.username = ?0 //위치 기반 select m from Member m where m.username = :name //이름 기반
import org.springframework.data.repository.query.Param
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
➡ 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용
컬렉션 파라미터 바인딩
Collection
타입으로 in절 지원
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names")List<String>names);
📌 반환 타입
스프링 데이터 JPA는 유연한 반환 타입 지원
List<Member> findByUsername(String name); //컬렉션 Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
조회 결과가 많거나 없으면?
- 컬렉션
- 결과 없음: 빈 컬렉션 반환
- 단건 조회
- 결과없음:
null
반환 - 결과가 2건 이상:
javax.persistence.NonUniqueResultException
예외 발생
- 결과없음:
cf) 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출
➡ 이 메서드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편함
➡ 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환
📌 순수 JPA 페이징과 정렬
예제
- 검색 조건: 나이가 10살
- 정렬 조건: 이름으로 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
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();
}
JPA 페이징 테스트 코드
@Test
public void paging() throws Exception{
//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 = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age,offset,limit);
long totalCount = memberJpaRepository.totalCount(age);
//페이지 계산 공식 적용...
// totalPage = totalCount / size ...
// 마지막 페이지 ...
// 최초 페이지 ..
//then
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
📌 스프링 데이터 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 쿼리 없이 결과만 반환
페이징과 정렬 사용 예제
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
예제
- 검색 조건: 나이가 10살
- 정렬 조건: 이름으로 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
Page 사용 예제 정의 코드
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
Page 사용 예제 실행 코드
@Test
public void page() throws Exception{
//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));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.ASC, "username");
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then
List<Member> content = page.getContent();//조회된 데이터
assertThat(content.size()).isEqualTo(3);//조회된 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0);//페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
assertThat(page.isFirst()).isTrue();//첫번째 항복인가?
assertThat(page.hasNext()).isTrue();//다음 페이지가 있는가?
}
- 두 번째 파라미터로 받은
Pageable
➡ 인터페이스다 - 실제 사용할 때는 해당 인터페이스를 구현한
org.springframework.data.domain.PageRequest
객체를 사용
PageRequest
생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력- 추가로 정렬 정보도 파라미터로 사용 가능
- 참고로 페이지는 0부터 시작
주의❗ : Page는 1부터 시작이 아니라 0부터 시작
Page 인터페이스
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
Slice 인터페이스
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가
boolean isLast(); //현재 페이지가 첫 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부 마지막 페이지 인지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
cf) count 쿼리를 다음과 같이 분리할 수 있음
@Query(value = "select m from Member m", countQuery = "select count(m.username) from Member m") Page<Member> findMemberAllCountBy(Pageable pageable);
Top, First 사용 참고
- 참고 링크
List<Member> findTop3By();
페이지를 유지하면서 엔티티를 DTO로 변환하기
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
실습
- Page
- Slice (count X) 추가로 limit + 1을 조회한다. 그래서 다음 페이지 여부 확인(최근 모바일 리스트 생각해보면 됨)
- List (count X)
- 카운트 쿼리 분리(이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됨)
- 실무에서 매우 중요!!!
cf): 전체 count 쿼리는 매우 무거움
📌 스프링 부트 3 - 하이버네이트 6 left join 최적화 설명 추가
- 스프링 부트 3 이상을 사용하면 하이버네이트 6이 적용됨
➡ 하이버네이트 6에서 의미없는 left join을 최적화 해버림
➡ SQL이 LEFT JOIN을 하지 않는 것으로 보임
@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);
실행 결과 - SQL
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
하이버네이트 6은 이런 경우 왜 left join을 제거하는 최적화를 할까?
- 실행한 JPQL을 보면 left join을 사용하고 있음
select m from Member m left join m.team t
- Member 와 Team 을 조인을 하지만 사실 이 쿼리를 Team 을 전혀 사용하지 않음
- select 절이나, where 절에서 사용하지 않는 다는 뜻
- select m from Member m
left join
이기 때문에 왼쪽에 있는member
자체를 다 조회한다는 뜻이 됨- 만약
select
나,where
에team
의 조건이 들어간다면 정상적인join
문이 보임 - JPA는 이 경우 최적화를 해서
join
없이 해당 내용만으로 SQL을 만듦 - 만약
Member
와Team
을 하나의 SQL로 한번에 조회하고 싶다면 JPA가 제공하는fetch join
을 사용select m from Member m left join fetch m.team t
- SQL에서 join문은 정상 수행
벌크성 수정 쿼리
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를 사용한 벌크성 수정 쿼리 테스트
@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
assertThat(resultCount).isEqualTo(3);
}
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age")int age);
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리 테스트
@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);
//then
assertThat(resultCount).isEqualTo(3);
}
- ❗ 벌크성 수정, 삭제 쿼리는
@Modifying
어노테이션을 사용- 사용하지 않으면 다음 예외 발생
org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화:
@Modifying(clearAutomatically = true)
- 이 옵션의 기본값은
false
- 이 옵션 없이 회원을
findById
로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있음- true로 해주면 자동으로 clear
- 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 (1차캐시에 데이터가 남아있음)
- 이 옵션의 기본값은
cf) 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있음
⚡ 권장하는 방안
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행
- 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화
📌 @EntityGraph
- 연관된 엔티티들을 SQL 한번에 조회하는 방법
- member -> team은 지연로딩 관계
- 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가 실행
- N+1 문제 발생
- N+1 문제 발생
@Test
public void findMemberLazy () throws Exception{
//given
// member1 -> teamA
// member2 -> teamB
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
teamRepository.save(teamA);
teamRepository.save(teamB);
memberRepository.save(new Member("member1", 10, teamA));
memberRepository.save(new Member("member2", 20, teamB));
em.flush();
em.clear();
//when
List<Member> members = memberRepository.findAll();
//then
for (Member member : members) {
member.getTeam().getName();
}
//Hibernate 기능으로 확인 Hibernate.isInitialized(member.getTeam())
//JPA 표준 방법으로 확인
PersistenceUnitUtil util =
em.getEntityManagerFactory().getPersistenceUnitUtil();
util.isLoaded(member.getTeam());
➡ 지연 로딩 여부를 확인 가능
JPQL 페치 조인
➡ 연관된 엔티티를 한번에 조회하려면 페치 조인이 필요
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
- 스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와줌
- JPQL 없이 페치 조인을 사용가능 (JPQL + 엔티티 그래프도 가능)
EntityGraph
//공통 메서드 오버라이드
@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)
EntityGraph 정리
- 사실상 페치 조인(FETCH JOIN)의 간편 버전
- LEFT OUTER JOIN 사용
NamedEntityGraph 사용 방법
🔶 Member 엔티티
@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}
🔶 MemberRepository
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
📌 JPA Hint & Lock
JPA Hint
- JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
쿼리 힌트 사용
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
쿼리 힌트 사용 확인
@Test
public void queryHint() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //Update Query 실행X
}
쿼리 힌트 Page 추가 예제
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
org.springframework.data.jpa.repository.QueryHints
어노테이션을 사용forCounting
: 반환 타입으로Page
인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값true
)
Lock
➡ 손대지 못하게
➡ 메뉴얼 확인하고 사용
➡ 좀 깊은 내용.....
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
org.springframework.data.jpa.repository.Lock
어노테이션을 사용
김영한님의 <실전! 스프링 데이터 JPA>을 수강하고 작성한 글입니다
PREV
NEXT
'인프런 Spring 강의 정리 > 실전! 스프링 데이터 JPA' 카테고리의 다른 글
[스프링 데이터 JPA] 6. 스프링 데이터 JPA 분석 (0) | 2024.01.24 |
---|---|
[스프링 데이터 JPA] 5. 확장 기능 (0) | 2024.01.24 |
[스프링 데이터 JPA] 3. 공통 인터페이스 기능 (0) | 2024.01.24 |
[스프링 데이터 JPA] 2. 예제 도메인 모델 (0) | 2024.01.24 |
[스프링 데이터 JPA] 1. 프로젝트 환경설정 (0) | 2024.01.24 |