: 메소드 이름을 분석해 JPQL 쿼리 실행하기
스프링 데이터 JPA는 메소드 이름을 분석해 JPQL을 생성하고 실행한다.
더 궁금한 내용은 스프링 데이터 JPA 공식 문서를 참고하자.
find...By
, read...By
, query...By
, get...By
...
에 식별하기 위한 내용(설명)이 들어가도 된다.)count...By
반환타입 longexists...By
반환타입 booleandelete...By
, remove...By
반환타입 longfindDistinct
, findMemberDistinctBy
findFirst3
, findFirst
, findTop
, findTop3
참고로 이 기능은 엔티티의 필드명이 변경되면 인터페이스에서 정의한 메소드 이름도 꼭 함께 변경해야 한다.
그렇지 않으면 애플리케이션 시작 시점 오류가 발생한다.
이렇게 애플리케이션 로딩 시점, 오류를 인지할 수 있다는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
: JPA NamedQuery를 호출할 수 있음
이 내용은 대충 듣기. 어차피 실무에서 안 씀 ㅎㅋㅎ
우선 @NamedQuery
어노테이션으로 Named 쿼리를 정의해야 한다. 그리고 JPA를 직접 사용해 Named 쿼리를 호출하거나 스프링 데이터 JPA로 NamedQuery 사용하기.
➡️ 후자는 @Query
를 생략하고 메소드 이름만으로 Named 쿼리를 호출할 수 있다.
스프링 데이터 JPA로 Named 쿼리를 호출할 때에는 기본적으로 엔티티에는 @NamedQuery
가 있어야 한다.
도메인 클래스 + . + 메소드 이름
으로 Named 쿼리를 찾아 실행한다.참고로,
스프링 데이터 JPA를 사용하면 실무에서 NamedQuery를 직접 등록해 사용하는 일은 드물다.(강사님도 대충 설명하심^^;)
대신 @Query
를 사용해 리포지토리 메소드에 쿼리를 직접 정의한다.
NamedQuery의 가장 큰 장점은
애플리케이션 로딩 시점 문법오류를 잡을 수 있다는 것이다.
: 인터페이스 메소드에 JPQL을 바로 작성할 수 있다.
@org.springframework.data.jpa.repository.Query
어노테이션을 사용하며참고로,
실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메소드 이름이 매우 지저분해지므로 @Query
기능을 자주 사용하게 된다.
DTO로 직접 조회하기 위해서는 JPA의 new
명령어가 필요하다. 그리고 생성자가 맞는 DTO 또한 필요하다. (JPA와 사용 방식이 동일)
// MemberDto.java
package study.datajpa.dto;
import lombok.Data;
@Data // getter, setter 다 들어있음 (사용 주의)
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 //이름 기반
하지만 위치 기반은 위치가 바뀔 수 있어 코드 가독성, 유지보수를 위해 이름 기반 @Param
을 사용하기
스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
List<Member> findByListUsername(String username); // 컬렉션
Member findMemberByUsername(String username); // 단건
Optional<Member> findOptionalByUsername(String username); // 단건 Optional을 반환하는 경우
이렇게 컬렉션, 단건, 단건 Optional을 반환하도록 할 수 있다.
📎 공식문서 참조
null
반환javax.persistence.NonUniqueResultException
예외 발생참고
단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult()
메소드를 호출한다.
이 메소드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException
예외가 발생하는데 개발자 입장에서 다루기 매우 불편하다.
스프링 데이터 JPA는 단건 조회할 때 이 예외가 발생하면 예외를 무시하고 대신 null을 반환한다.
: 검색 조건(나이 10), 정렬 조건(이름 기준 내림차순), 페이징 조건(첫 번째 페이지, 페이지 당 보여줄 데이터 3건)
(페이징을 공통화 시킴)
org.springframework.data.domain.Sort
: 정렬 기능
org.springframework.data.domain.Pageable
: 페이징 기능(내부에 Sort 포함)
스프링 데이터는 이렇게 sort, pageable로 끝냄
org.springframework.data.domain.Page
: 추가 count 쿼리 결과를 포함하는 페이징(total count 쿼리가 함께 나감)
org.springframework.data.domain.Slice
: 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit+1 조회. 더보기처럼)
List
(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환
shift + fn + F6
: 한꺼번에 Rename 시킬 수 있음
// when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
// Page면 totalcount 쿼리를 같이 날리므로 코드가 따로 필요없음
// then
List<Member> content = page.getContent();
assertThat(content.size()).isEqualTo(3); // 3개까지 끌어와야하니까 3
assertThat(page.getTotalElements()).isEqualTo(5); // 총 멤버 5명
assertThat(page.getNumber()).isEqualTo(0); // getNumber: 페이지 번호를 가져옴.
assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지는 멤버 셋(첫페이지) 멤버 둘(다음 페이지) => 총 페이지 2개
assertThat(page.isFirst()).isTrue(); // 당연히 첫번째 페이지니까 ㅇㅇ
assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있냐 ㅇㅇ
Pageable은 인터페이스. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest
객체를 사용해야 함.
PageRequest
생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있음.
그리고 Page는 1부터 시작이 아니라 0부터 시작임!!!
slice의 경우
assertThat(page.getTotalElements()).isEqualTo(5); // 총 멤버 5명
assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지는 멤버 셋(첫페이지) 멤버 둘(다음 페이지) => 총 페이지 2개
이런 기능이 없기 때문에 이 두 코드를 검증할 수 없음.
Slice<Member> findByAge(int age, Pageable pageable);
Page<MemberDto> toMap = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
🏢: 모든 직업의 월급을 10%씩 인상해!!
이런 상황이 되면 하나씩 처리하는 것보다는 DB에 업데이트 쿼리를 날려 한꺼번에 처리하는 것이 낫다.
→ 벌크성 수정쿼리
command + option + N
: 인라인
벌크성 수정, 삭제 쿼리는 @Modifying
어노테이션을 사용한다.
사용하지 않으면 org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
오류가 발생한다.
벌크성 쿼리를 실행하고 난 뒤 영속성 컨텍스트를 초기화해주어야 한다. (아래에서 설명)
그래서 영속성 컨텍스트 안 엔티티 상태와 DB 엔티티 상태가 달라질 수 있음
이를 해결하기 위해서는
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화
이 결과는 2번을 실행한 내용이다.
그런데
flush(), clear() 이렇게 안 하고 @Modifying
으로 영속성 컨텍스트를 초기화 시켜줄 수도 있다.
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
: 연관된 엔티티들을 SQL 한 번에 조회하는 방법
member → team은 지연로딩 관계
따라서 team 데이터를 조회할 때마다 쿼리가 실행돼 N + 1 문제가 발생한다.
연관된 엔티티를 한 번에 조회하기 위해서는 fetch join이 필요하다.
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하도록 도와준다. 이 기능을 사용하면 JPQL 없이 fetch join을 사용할 수 있다.
(JPQL + 엔티티 그래프도 가능)
/**
* 공통 메소드 오버라이드
*/
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
/**
* JPQL + 엔티티그래프
*/
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
/**
* 메소드 이름으로 쿼리에서 특히 편리
*/
// @EntityGraph(attributePaths = {"team"})
@EntityGraph("Member.all")
List<Member> findEntityGraphByUsername(@Param("username") String username);
즉, EntityGraph는 사실상 FETCH JOIN의 간편 버전이다.
그리고 LEFT OUTER JOIN을 사용한다.
: JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
org.springframework.data.jpa.repository.QueryHints
어노테이션을 사용
forCounting
: 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true)
org.springframework.data.jpa.repository.Lock
어노테이션을 사용