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

HOONEY·2022년 5월 31일
0

Java

목록 보기
16/20
post-thumbnail

김영한님의 실전! 스프링 데이터 JPA 정리

메소드 이름으로 쿼리 생성

  • 메소드 이름을 분석해서 JPQL 쿼리 생성
  • 이름과 나이를 기준으로 회원을 조회하려면?

순수 JPA 리포지토리

스프링 데이터 JPA

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

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

  • 조회: find ... by, read ... by, query ... by, get... by
  • COUNT: count ... by - 반환타입 long
  • EXIST: exists ... by - 반환타입 boolean
  • 삭제: delete ... by, remove ... by - 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3
    참고: 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 꼭 함께 변경해야함.

JPA NamedQuery

  • JPA의 NamedQuery를 호출할 수 있음
  • 실무에서는 잘 사용하지 않는다. ***

@Query, 리포지토리 메소드에 쿼리 정의하기

  • 메소드에 JPQL 쿼리 작성

  • @org.springframework.data.jpa.repository.Query 어노테이션 사용

  • 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름없는 Named 쿼리라고 할 수 있음

  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음!
    참고: 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메소드 이름이 매우 지저분해진다. 따라서 @Query기능을 자주 사용하게 된다.

@Query, 값, DTO 조회하기

  • 단순히 값 하나를 조회
@Query("select m.username from Member m")
    List<String> findUsernameList();
  • 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와 사용방식이 동일하다)

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

파라미터 바인딩

  • 위치 기반 -> 사용 금지
  • 이름 기반

파라미터 바인딩

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> findListByUsername(String name); //컬렉션
    Member findMemberByUsername(String name); //단건
    Optional<Member> findOptionalByUsername(String name); //단건 Optional

조회 결과가 많거나 없으면?

  • 컬렉션:
    결과 없음: 빈 컬렉션 반환
  • 단건 조회:
    결과 없음: null 반환
    결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생

참고: 단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult()메소드를 호출한다. 이 메소드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.

순수 JPA 페이징과 정렬

  • JPA에서 페이징을 어떻게 할 것인가?

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 페이징과 정렬

페이징과 정렬 파라미터

  • 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);

주의: Page는 1부터 시작이 아니라 0부터 시작이다.

페이지를 유지하면서 엔티티를 DTO로 변환하기

Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

벌크성 수정 쿼리

  • 벌크성 수정, 삭제 쿼리는 @Modyfying 어노테이션 사용
  • 사용 안할 시 에러
  • 벌크성 쿼리 실행하고 영속성 컨텍스트 초기화: @Modyfying(clearAutomatically = true)
  • 다시 조회할 경우 꼭 컨텍스트 초기화 하기.
    참고: 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
    권장하는 방안
  1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
  2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

@EntityGraph

  • 연관된 엔티티들을 SQL 한번에 조회하는 방법
  • 사실상 페치조인(FETCH JOIN)의 간편 버젼
//공통 메서드 오버라이드
@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)

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
}
  • Lock

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
profile
기록하는 블로그

0개의 댓글