쿼리 메소드 기능 3가지
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 Repository 인터페이스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
메소드 이름을 분석해서 JPQL 쿼리 실행
#이름과 나이를 기준으로 회원을 조회하려면?
public List<Member> findByUsernameAndGreaterThan(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();
}
Spring Data Jpa
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
- 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고 : https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.query-creation
스프링 데이터 JPQ가 제공하는 쿼리 메소드 기능
- 조회 : find...By, read...By, query...By, get...By
- https://docs.spring.io/spring-data/jpa/reference/repositories/query-keywords-reference.html
- 예:) findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
- By 다음에 아무것도 없으면 전체 조회이다.
- COUNT : count...By 반환 타입 long
- EXISTS: exists...By 반환타입 boolean
- 삭제: delete...By, remove...By 반환타입 long
- DISTINCT: findDistinct, findMemberDistinctBy
- LIMIT: findFrist, findFrst3, findTop, findTop3
- Order: findByLastnameOrderByFirstnameDesc(String lastname)
JPA NamedQuery
JPA의 NamedQuery를 호출할 수 있음
@NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@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> {
List<Member> findByUsername(@Param("username") String username);
}
- 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
- 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
- NamedQuery가 우선 순위
- 필요하면 전략을 변경할 수 있지만 권장하지 않는다.
JPA Query Methods :: Spring Data JPA
As of Spring Data JPA release 1.4, we support the usage of restricted SpEL template expressions in manually defined queries that are defined with @Query. Upon the query being run, these expressions are evaluated against a predefined set of variables. Sprin
docs.spring.io
@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 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음 (매우 큰 장점!)
참고 : "메소드 이름으로 쿼리 생성" 기능은 파라미터가 증가할 경우, 메서드 이름이 매우 지저분해진다.
따라서 실무에서는 @Query 기능을 자주 사용하게 된다.
@Query 값, DTO 조회하기
단순히 값 하나를 조회
@Query("select m.username from Member m")
List<String> findUsernameList();
- JPA 값 타임(@Embedded)도 이 방식으로 조회할 수 있다.
DTO로 직접 조회
- 주의! DTO로 직접 조회하려면 JPA의 new 명령어를 사용해야 한다. 그리고 다음과 같이 생성자가 맞는 DTO가
필요하다. (JPA와 사용 방식 동일)
package study.datajpa.repository;
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;
}
}
파라미터 바인딩
- 위치 기반
- 이름 기반
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);
}
// name이 list 형식일 때
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> username);
}
- 참고 : 코드 가독성과 유지 보수를 위해 이름 기반 파라미터 바인딩을 사용하자 (위치 기반은 실수로 순서가 바뀔 수도)
반환 타입
스프링 데이터 JPA는 유연한 반환타입 지원
List<Member> findByUsername(String name); // 컬렉션
Member findByUsername(String name); // 단건
Optional<Member> findByUsername(String name); //단건 Optional
스프링 데이터 JPA 공식 문서(반환타입) : https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html
Repository query return types :: Spring Data JPA
The following table lists the return types generally supported by Spring Data repositories. However, consult the store-specific documentation for the exact list of supported return types, because some types listed here might not be supported in a particula
docs.spring.io
조회 결과가 많거나 없으면?
- 컬렉션
- 결과 없음 : 빈 컬렉션 반환
- 단건 조회
- 결과 없음 : null 반환
- 결과가 2건 이상 : javax.persistence.NonUniqueResultException 예외 발생
참고 : 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다.
이 메서드를 호출했을 때, 조회 결과가 없으면javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외를 무시하고 대신에 null을 반환한다.
스프링 데이터 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 Reository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
// PageRequest 사용 예시
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page 사용 시 주의할 점
- findByAge에서 두 번째 파라미터로 받은 Pageable은 인터페이스이다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
- PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.
- 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); //변환기
}
Page와 Slice를 사용할 시점
- Page : 전체 데이터 개수, 페이지 수 등을 포함, 추가적인 쿼리로 전체 데이터 개수를 조회
- Slice : 다음 페이지가 있는지 여부만 알 수 있으며, 전체 데이터 개수 조회하지 않기 때문에 더 가볍고 우수한 성능
- 전체 데이터 양을 알고, 페이징 내비게이션이 필요한 경우 Page, 무한 스크롤이나 "더 보기" 같은 기능 구현 필요 시 Slice 사용
참고 : count 쿼리를 다음과 같이 분리할 수 있음
Query에서 left join을 사용할 시, where문에서 필터링이 없는 countQuery는 join 하지 않는 것과 동일하기에 Query를 분리함으로써 성능 개선 가능
@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 로 변환하기(엔티티 자제를 return 하면 안된다. 스펙 변경시 위험)
Page<Member> Page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
스프링 부트 3 - 하이버네이트 6 left join 최적화 설명
하이버네이트 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을 보면 team을 조인하지만 select 절이나 where 절에서 team을 사용하지 않는다.
이 경우 select m from Member m과 동일하다.
만약 Member와 Team을 하나의 SQL로 조회하고 싶으면 JPA가 제공하는 fetch join을 사용해야 한다.
select m from Member m left join fetch m.team t
벌크성 수정 쿼리
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션 사용
- @Modifying 어노테이션 사용시 JPA의 executeUpdate()를 사용
- 사용하지 않으면 다음 예외 발생
- org.hibernate.hql.internal.QueryExecutionRequestException : Not supported for DML operations
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화 : @Modifying(clearAutomatically = true)
(이 옵션의 기본값은 false)- 이 옵션 없이 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화하자.
참고 : 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화한다.
@EntityGraph
연관된 엔티티들을 SQL 한 번에 조회하는 방법
참고 : 지연 로딩 여부 확인 방법
// 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(@Param("username") String username);
EntityGraph 정리
- 사실상 페치 조인(FETCH JOIN)의 간편 버전
- LEFT OUTER JOIN 사용
NamedEntityGraph 사용 방법
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
@Entity
public class Member {}
@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);
쿼리 힌트 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.jpq.repository.Lock 어노테이션 사용
'Spring > Spring Data Jpa' 카테고리의 다른 글
확장 기능 (사용자 정의 레포, Auditing, 도메인 클래스 컨버터, 페이징과 정렬) (2) | 2024.09.08 |
---|---|
Spring Data Jpa 공통 인터페이스 설정, 적용 및 분석 (2) | 2024.09.05 |