메소드 이름으로 쿼리 생성
순수 JPA를 사용하여 메소드를 작성하면 아래와 같으며 이 메소드는 입력된 사용자의 이름과 나이를 기준으로 Member 엔티티를 검색한다.
public List<Member> findByUsernameAndAgeGreaterThen(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를 사용하여 같은 쿼리를 작성해보겠다. Spring Data JPA를 사용하면 구현체 없이 인터페이스만으로 간단하게 쿼리를 생성할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
Spring Data JPA는 위와 같이 메소드 이름만으로 쿼리를 생성하고 실행한다. 즉, 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.
쿼리 메소드 필터 조건
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
위 문서를 참고하여 메소드 이름으로 쿼리를 생성할 수 있다.
Spring Data JPA가 제공하는 쿼리 메소드 기능
- 조회 : find...By, read...By, query...By, get...By
- COUNT : count...By (반환 타입: long)
- EXISTS : exists...By (반환 타입: boolean)
- 삭제 : delete...By, remove...By (반환 타입: long)
- DISTINCT : findDistinct, findMemberDistinctBy
- LIMIT : findFirst3, findFirst, findTop, findTop3
주의할 점으로 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다. 이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것은 Spring Data JPA의 매우 큰 장점이다.
JPA NamedQuery
Member 클래스에서 유저 이름에 따라 데이터를 조회하는 NamedQuery를 생성해보겠다. @NamedQuery 어노테이션을 사용하여 정의한다.
@Entity
@NamedQuery( // 애플리케이션 로딩 시점에 파싱됨 -> 오류가 있다면 애플리케이션 실행 불가(
name="Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
public class Member {}
위와 같이 NamedQuery를 작성했다면 JPA를 직접 사용해서 아래처럼 MemberRepository에서 NamedQuery를 호출한다.
public List<Member> findByUsername(String username) {
return em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
그럼 이번에는 Spring Data JPA로 NamedQuery를 활용해보겠다. 아래의 예제에서는 MemberRepository에서 @Query 어노테이션을 사용하여 NamedQuery를 호출한다
public interface MemberRepository extends JpaRepository<Member, Long> {
//@Query(name = "Member.findByUsername") // 이게 없어도 "메소드 이름으로 쿼리 생성"을 우선적으로 처리하기 때문에 생략이 가능하다
List<Member> findByUsername(@Param("username") String username); // m.username = :username 처럼 명확하게 JPQL을 작성했을 때 @Param으로 값을 넘겨야함
}
또한, @Query 어노테이션을 생략하고, 메소드 이름만을 이용해 NamedQuery를 호출하는 것도 가능하다. Spring Data JPA는 "도메인 클래스 + .(점) + 메서드 이름"의 규칙으로 NamedQuery를 찾아서 실행한다. 만약 해당 NamedQuery가 없다면, 메서드 이름을 바탕으로 쿼리 생성 전략을 사용한다. 이러한 전략을 변경할 수도 있지만 권장하지 않는다.
Spring Data JPA를 사용하면, 일반적으로는 NamedQuery를 직접 등록해서 사용하는 일은 드물다. 대신, @Query를 사용하여 리포지토리 메소드에 쿼리를 직접 정의한다.
Query, 리포지토리 메소드에 쿼리 정의하기
Spring Data JPA에서는 메소드 이름으로 간단한 쿼리를 생성해주는 기능이 있지만 복잡한 쿼리를 작성해야 한다면 findByUsernameAndYearAndAgeGreaterThan 처럼 너무 길어질 수 있다. 이 경우, 가독성이 떨어지고 메서드 이름이 지저분해진다.
이 때, @Query를 사용하면 큰 도움이 된다.
@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);
이렇게 메서드에 직접 쿼리를 작성하면 이름 없는 NamedQuery라고 할 수 있다. 이 방식의 장점은 NamedQuery처럼 애플리케이션 실행 시점에 문버버 오류를 미리 확인할 수 있다.
@Query, 값, DTO 조회하기
@Query를 사용하면 이전처럼 엔티티 뿐만 아니라 단순 값 혹은 DTO도 쉽게 조회할 수 있다.
@Query("select m.username from Member m")
List<String> findUsernameList();
위 쿼리는 단순히 값 하나를 조회하는 쿼리로 JPA 값 타입(@Embedded)도 이 방식으로 조회할 수 있다.
/** MemberRepository */
Query("select new study.data_jpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t") // dto로 반환
List<MemberDto> findMemberDto();
/** MemberDto */
@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;
}
}
위 쿼리는 DTO로 직접 조회할 때의 방식으로 이 때는 JPA의 new 명령어를 사용해야 한다.
그리고 DTO에서의 생성자의 순서와 쿼리에서 select하는 컬럼의 순서(m.id, m.username, t.name)가 일치해야 한다.
파라미터 바인딩
파라미터 바인딩은 위치 기반과 이름 기반으로 구분된다.
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반
위치 기반 파라미터 바인딩은 ? 뒤에 인덱스를 붙여서 사용하는데 이 방식은 파라미터의 순서가 바뀌면 쿼리의 의미가 달라질 수 있으므로 사용하지 말자.
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
이름 기반 파라미터 바인딩은 : 뒤에 쿼리 내부에서 사용할 변수명을 붙여 사용한다. 이후 @Param 어노테이션을 사용하여 메서드 파라미터와 매핑시킨다.
이 방식을 사용하면 쿼리의 의미가 바뀌지 않기 때문에 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용해야 된다.
반환 타입
Spring Data JPA는 유연한 반환 타입을 지원한다.
List<Member> findListByUsername(String name); // 컬렉션 반환
Member findMemberByUsername(String name); // 단건 반환
Optional<Member> findOptionalByUsername(String username); // 단건 Optional 반환
- 컬렉션 반환 : 조회 결과가 여러 개일 때 사용한다.
- 단건 반환 : 조회 결과가 하나일 때 사용한다.
- 단건 Optional 반환 : 조회 결과가 없을 경우 Optional.empty를 반환한다. 예외를 발생시키지 않으므로 안전하게 데이터 처리가 가능하다.
조회 결과가 많거나 없을 때처럼, 조회 결과에 따라 반환 타입이 어떻게 처리되는지에 대한 결과는 아래와 같다.
- 컬렉션 반환
- 결과 없음 : 빈 컬렉션 반환
- 단건 조회
- 결과 없음 : null 반환
- 결과가 2건 이상 : org.hibernate.NonUniqueResultException 예외 발생
참고: 단건으로 지정한 메서드를 호출하면 Spring Data JPA는 내부에서 JPQL의 Query.getSingleResult()를 호출한다. 이 메서드를 조회했을 때, 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자가 다루기에는 상당히 불편하기 때문에 Spring Data JPA는 단건을 조회할 때, 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.
[출처]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84 → 이 글은 김영한님의 "실전! 스프링 데이터 JPA" 강의 중 5장을 듣고 정리한 내용입니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[Spring Data JPA] 2-3. 벌크성 수정 쿼리 (0) | 2024.09.03 |
---|---|
[Spring Data JPA] 2-2. JPA 페이징과 정렬 (0) | 2024.08.30 |
[Spring Data JPA] 1. 공통 인터페이스 기능 (0) | 2024.08.24 |
[JPA 활용 2] 4. OSIV와 성능 최적화 (0) | 2024.08.17 |
[JPA 활용 2] 3. API 개발 고급 - 컬렉션 조회 최적화 (0) | 2024.08.17 |