본문 바로가기

JPA 실전

[JPA] JPA 실전 - 쿼리 메소드 (1)

 

 

 

쿼리 메소드 기능 3가지

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의

 

이름과 나이를 기준으로 회원을 조회하려면?

 

순수 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

 public interface MemberRepository extends JpaRepository<Member, Long> {
     List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

 

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

조회 (Find, Read, Query, Get)

  • 형식: find...By, read...By, query...By, get...By
  • 설명: 특정 조건에 맞는 데이터를 조회합니다. 예를 들어, findHelloBy처럼 쿼리 메소드 이름에 식별하기 위한 내용을 포함할 수 있습니다.
List<Member> findByLastName(String lastName);
Member findByEmail(String email);

 

COUNT

  • 형식: count...By
  • 반환 타입: long
  • 설명: 특정 조건에 맞는 데이터의 개수를 반환합니다.
long countByLastName(String lastName);

 

EXISTS

  • 형식: exists...By
  • 반환 타입: boolean
  • 설명: 특정 조건에 맞는 데이터가 존재하는지 확인합니다.
boolean existsByEmail(String email);

 

삭제 (Delete, Remove)

  • 형식: delete...By, remove...By
  • 반환 타입: long
  • 설명: 특정 조건에 맞는 데이터를 삭제합니다.
long deleteByLastName(String lastName);
long removeByEmail(String email);

 

DISTINCT

  • 형식: findDistinct, findMemberDistinctBy
  • 설명: 중복되지 않는 결과를 반환합니다.
List<Member> findDistinctByLastName(String lastName);

 

LIMIT

  • 형식: findFirst3, findFirst, findTop, findTop3
  • 설명: 결과를 제한하여 상위 몇 개의 데이터를 반환합니다.
List<Member> findFirst3ByOrderByLastNameAsc();
Member findTopByOrderByAgeDesc();

 

참고사항

이러한 메소드들은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 함께 변경해야 합니다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생합니다. 이로 인해 애플리케이션 로딩 시점에 오류를 인지할 수 있다는 점이 스프링 데이터 JPA의 큰 장점입니다.

 

스프링 데이터 JPA 공식 문서 : https://docs.spring.io/spring-data/jpa/reference/#jpa.query-methods.query-creation

 

Spring Data JPA :: Spring Data JPA

Oliver Gierke, Thomas Darimont, Christoph Strobl, Mark Paluch, Jay Bryant, Greg Turnquist Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that

docs.spring.io

 

 

JPA NamedQuery

JPANamedQuery를 호출할 수 있음

 

@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)
	} 
}

 

 

스프링 데이터 JPA로 NamedQuery 사용

@Query(name = "Member.findByUsername")
 List<Member> findByUsername(@Param("username") String username);

`@Query` 를 생략하고 메서드 이름만으로 Named 쿼리를 호출할 수 있다.

 

**스프링 데이터 JPANamed 쿼리 호출**

public interface MemberRepository
extends JpaRepository<Member, Long> { //** 여기 선언한 Member 도메인 클래스
     List<Member> findByUsername(@Param("username") String username);
 }
  • 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
  • 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
  • 필요하면 전략을 변경할 수 있지만 권장하지 않는다.

참고: 스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다. 대신 `@Query` 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.

 

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

메서드에 JPQL 쿼리 작성**

@Repository
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로 직접 조회**

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

 

 

파라미터 바인딩

- 위치 기반

- 이름 기반

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` 예외 발생

 

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

 

 

순수 JPA 페이징과 정렬
JPA에서 페이징을 어떻게 할 것인가?
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.

검색 조건: 나이가 10
정렬 조건
: 이름으로 내림차순
페이징 조건
: 첫 번째 페이지, 페이지당 보여줄 데이터는 3