[스프링 데이터 JPA] 쿼리 메소드 기능

윤경·2021년 11월 6일
0

JPA

목록 보기
17/22
post-thumbnail
post-custom-banner

[1] 메소드 이름으로 쿼리 생성

: 메소드 이름을 분석해 JPQL 쿼리 실행하기

스프링 데이터 JPA는 메소드 이름을 분석해 JPQL을 생성하고 실행한다.

더 궁금한 내용은 스프링 데이터 JPA 공식 문서를 참고하자.

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

  • 조회: find...By, read...By, query...By, get...By
    (findHelloBy처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.)
  • COUNT: count...By 반환타입 long
  • EXISTS: exists...By 반환타입 boolean
  • 삭제: delete...By, remove...By 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

참고로 이 기능은 엔티티의 필드명이 변경되면 인터페이스에서 정의한 메소드 이름도 꼭 함께 변경해야 한다.
그렇지 않으면 애플리케이션 시작 시점 오류가 발생한다.

이렇게 애플리케이션 로딩 시점, 오류를 인지할 수 있다는 것이 스프링 데이터 JPA의 매우 큰 장점이다.


[2] (잘 사용 X)JPA NamedQuery

: JPA NamedQuery를 호출할 수 있음

이 내용은 대충 듣기. 어차피 실무에서 안 씀 ㅎㅋㅎ

우선 @NamedQuery 어노테이션으로 Named 쿼리를 정의해야 한다. 그리고 JPA를 직접 사용해 Named 쿼리를 호출하거나 스프링 데이터 JPA로 NamedQuery 사용하기.
➡️ 후자는 @Query를 생략하고 메소드 이름만으로 Named 쿼리를 호출할 수 있다.

스프링 데이터 JPA로 Named 쿼리를 호출할 때에는 기본적으로 엔티티에는 @NamedQuery가 있어야 한다.

없어도 잘 동작

  • 스프링 데이터 JPA는 선언한 도메인 클래스 + . + 메소드 이름으로 Named 쿼리를 찾아 실행한다.
  • 만약 실행할 Named 쿼리가 없다면 메소드 이름으로 쿼리 생성 전략을 사용
  • 필요하면 전략을 변경할 수 있지만 권장하지 않는다.

참고로,
스프링 데이터 JPA를 사용하면 실무에서 NamedQuery를 직접 등록해 사용하는 일은 드물다.(강사님도 대충 설명하심^^;)
대신 @Query를 사용해 리포지토리 메소드에 쿼리를 직접 정의한다.

NamedQuery의 가장 큰 장점은
애플리케이션 로딩 시점 문법오류를 잡을 수 있다는 것이다.


[3] (많이 사용)@Query, 리포지토리 메소드에 쿼리 정의하기

: 인터페이스 메소드에 JPQL을 바로 작성할 수 있다.

  • @org.springframework.data.jpa.repository.Query 어노테이션을 사용하며
  • 실행할 메소드에 정적 쿼리를 직접 작성해 이름 없는 Named 쿼리라고 할 수 있다.
  • JPA Named 쿼리처럼 어플리케이션 실행 시점 문법 오류를 발견할 수 있다.(매우 큰 장점)

참고로,
실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메소드 이름이 매우 지저분해지므로 @Query 기능을 자주 사용하게 된다.


[4] (많이 사용)@Query, 값, DTO 조회하기

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;
    }
}

테스트 결과


[5] 파라미터 바인딩

파라미터 바인딩에는 위치 기반이름 기반이 있다.

select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반

하지만 위치 기반은 위치가 바뀔 수 있어 코드 가독성, 유지보수를 위해 이름 기반 @Param을 사용하기


[6] 반환 타입

스프링 데이터 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을 반환한다.


[7] 순수 JPA 페이징과 정렬

: 검색 조건(나이 10), 정렬 조건(이름 기준 내림차순), 페이징 조건(첫 번째 페이지, 페이지 당 보여줄 데이터 3건)


[8] 스프링 데이터 JPA 페이징과 정렬

페이징과 정렬 파라미터

(페이징을 공통화 시킴)
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 시킬 수 있음

Paging

⬇️ 검증시 이렇게 검증하면 된다.

// 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

slice의 경우

assertThat(page.getTotalElements()).isEqualTo(5);   // 총 멤버 5명
assertThat(page.getTotalPages()).isEqualTo(2);      // 전체 페이지는 멤버 셋(첫페이지) 멤버 둘(다음 페이지) => 총 페이지 2개

이런 기능이 없기 때문에 이 두 코드를 검증할 수 없음.

물론!! MemberRepository.java의 Page → Slice로 바꿔줘야 함

    Slice<Member> findByAge(int age, Pageable pageable);

페이지 유지하며 엔티티 → DTO

Page<MemberDto> toMap = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));

[9] 벌크성 수정 쿼리

🏢: 모든 직업의 월급을 10%씩 인상해!!

이런 상황이 되면 하나씩 처리하는 것보다는 DB에 업데이트 쿼리를 날려 한꺼번에 처리하는 것이 낫다.
벌크성 수정쿼리

단축키

command + option + N: 인라인

벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용한다.
사용하지 않으면 org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations 오류가 발생한다.

벌크성 쿼리를 실행하고 난 뒤 영속성 컨텍스트를 초기화해주어야 한다. (아래에서 설명)

순수 JPA일 때

스프링데이터 JPA일 때

그런데 벌크 연산은 영속성 컨텍스트를 무시하고 실행

그래서 영속성 컨텍스트 안 엔티티 상태와 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);

[10] @EntityGraph

: 연관된 엔티티들을 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을 사용한다.


[11] JPA Hint & Lock

JPA Hint

: JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)

org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
forCounting: 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true)

Lock

org.springframework.data.jpa.repository.Lock 어노테이션을 사용


profile
개발 바보 이사 중
post-custom-banner

0개의 댓글