쿼리 메서드
스프링 데이터 JPA는 이전 포스트에서 설명했던 공통 메서드에 더불어 어떠한 커스텀 메서드까지 알아서 구현해준다.
예를 들어 이름과 나이를 기준으로 회원을 조회하고 싶다?
1
2
3
4
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(Sring username);
List<Member> findByUsernameAndGreaterThan(Sring username, int age);
}
findByUsernameAndAgeGreaterThan() 이러한 메서드를 구현하지 않고 네이밍만 하면 알아서 JPQL을 생성하고 실행해준다.
이것이 쿼리 메서드 기능이다.
쿼리 메서드 기능에는 3가지가 있다.
- 메서드 이름으로 쿼리 생성
- 메서드 이름으로 JPA NamedQuery 호출
- @Query 애노테이션을 사용해 리포지토리 인터페이스에 쿼리를 직접 정의하는 방식
메서드 이름으로 쿼리 생성
메서드 이름을 분석해서 JPQL 쿼리를 알아서 실행해주는 방법이다.
순수 JPA로 아래와 같은 코드를 짰다고 가정하자.
1
2
3
4
5
6
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 코드를 스프링 데이터 JPA를 이용한다면?
1
2
3
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
이렇게 선언하는 것만으로도 순수 JPA 코드와 같은 동작을 만들어낸다.
이 방법의 문제는 조건이 2개가 넘어갈 경우 계속해서 And … 이런식으로 메서드 명이 길어지는 문제가 있는데 이는 JPQL을 직접 짜는 방식으로 해결한다.
쿼리 메서드 필터 조건
필터 조건은 공식 사이트를 참고하자.
And, Or, In 등의 정보가 나타나있다. 필요 시 찾아서 사용하면 된다.
- 조회: find…By, read…By, query…By, get…By
- …에는 식별하기 위한 내용이 들어가도 된다.
- findHelloBy()
- By뒤에 where절 같은 조건이 붙으면 된다.
- COUNT: count…By
- 반환타입 = Long
- EXISTS: exists…By
- 반환타입 = boolean
- 삭제: delete…By, remove…By
- 반환타입 = Long
- DISTINCT: findDistinct, findMemberDistinctBy
- LIMIT: findFirst3, findFirst, findTop, findTop3
참고로 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 반드시 함께 변경해야 한다.
그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생할 수 있다.
이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 점도 스프링 데이터 JPA의 큰 장점이다.
JPA NamedQuery
거의 사용할 일이 없다.
1
2
3
@Entity
@NamedQuery( name="Member.findByUsername", query="select m from Member m where m.username = :username")
public class Member { ... }
1
2
@Query(name = "Member.findByUsername")
List findByUsername(@Param("username") String username);
쿼리를 직접 정의하고 싶은 경우에는 아래의 @Query 기능을 사용하기 때문이다.
@Query - 리포지토리 메서드에 쿼리 직접 정의
1
2
3
4
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 애노테이션을 사용
- 실행할 메서드에 정적 쿼리를 직접 작성한다.
- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
- 매우 큰 장점이다.
실무에서 메서드 이름으로 쿼리 생성 기능보다 @Query 기능을 자주 사용하게 된다.
위에서 말했던 단점인 파라미터가 증가했을 때 메서드 이름이 지저분해지는 단점 때문이다.
@Query - 값, DTO 조회
- 단순 값 하나 조회
1
2
@Query("select m.username from Member m")
List<String> findUsernameList();
JPA 값 타입(@Embedded)도 이러한 방식으로 조회가 가능하다.
- DTO로 직접 조회
1
2
3
4
5
6
7
8
9
10
11
12
13
@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;
}
}
1
2
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
파라미터 바인딩
- 위치 기반
- 이름 기반
2가지 파라미터 바인딩이 존재한다.
1
2
select m from Member m where m.username = ?0 //위치기반
select m from Member m where m.username = :name //이름기반
1
2
@Query("select m from Member m where m.username = :username)
Member findMember(@param("name") String username);
코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 권장한다.
컬렉션 파라미터 바인딩
Collection 타입으로 in절을 지원한다.
1
2
@Query("select m from Member m where m.username in :names)
List<Member> findByNames(@Param("names") List<String> names);
반환 타입
스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
1
2
3
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
조회 결과가 많거나 없다면?
- 컬렉션
- 결과 없음 = 빈 컬렉션 반환
- 단건 조회
- 결과 없음 = null 반환
- 결과가 2건 이상 = NonUniqueResultException 발생
참고: 단건 조회 시 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다.
이 메서드를 호출했을 때 조회결과가 없으면 NoResultException이 발생하는데 이는 개발자가 다루기 불편한 점이다.
스프링 데이터 JPA는 단건 조회 시 이 예외가 발생하면 예외를 무시하고 null을 반환한다.