쿼리 메소드 기능
- 메소드 이름만으로 쿼리를 생성하는 기능이 있어
인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행 - 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
- 메소드 이름으로 쿼리 생성
예) 이메일과 이름으로 회원을 조회하려면 다음과 같은 메소드를 정의
인터페이스에 정의한 메소드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
이를 위해서는 정해진 규칙에 따라 메소드 이름을 지어야 하므로 스프링 데이터 JPA 공식 문서가 제공하는 표를 이해해야 함
public interface MemberRepository extends Repository<Member, Long> {
List<Member> findByEmailAndName(String email, String name);
// select m from Member m where m.email =?1 and m.name = ?2
}
- JPA NamedQuery
쿼리에 이름을 부여해서 사용하는 방법으로 어노테이션이나 XML에 쿼리를 정의할 수 있음
// @NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
// orm.xml의 XML 사용
<named-query name="Member.findByUsername">
<query><CDATA[
select m
from Member m
where m.username = :username
]></query>
</named-query>
// JPA를 직접 사용해서 Named 쿼리 호출
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
}
}
// 스프링 데이터 JPA로 Named 쿼리 호출
public interface MemberRepository extedns JpaRepository<Member, Long> { // 여기 선언한 Member 도메인 클래스
// 스프링 데이터 JPA는 선언한 '도메인 클래스 + . + 도메인 이름'으로 Named 쿼리를 찾아서 실행
// 여기서는 Member.findByUsername이라는 Named 쿼리를 실행
// 만약 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용
List<Member> findByUsername(@Parma("username") String username); // 이름 기반 파라미터를 바인딩
}
- @Query, 리포지토리 메소드에 쿼리 정의
리포지토리 메소드에 직접 쿼리를 정의하기 위해 @Query 어노테이션을 사용해 실행할 메소드에 정적 쿼리를 직접 작성
직접 작성하므로 이름 없는 Named 쿼리라고 할 수 있으며 JPA Named 쿼리처럼 실행 시점에 문법 오류를 발견할 수 있음
네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true를 설정
참고로 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만
네이티브 SQL은 0부터 시작
// 메소드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = ?1")
Member findByUsername(String username);
{
// JPA 네이티브 SQL 지원
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
Member findByUsername(String username);
}
- 파라미터 바인딩
스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원
기본값은 위치 기반인데 파라미터 순서로 바인딩하며 이름 기반 파라미터 바인딩을 사용하려면 @Param 어노테이션을 사용
코드 가독성과 유지보수를 위해 이름 파라미터 바인딩을 사용
select m from Member m where m.username = ?1 // 위치 기반
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 findByUsername(@Param("name") String username);
}
- 벌크성 수정 쿼리
스프링 데이터 JPA로 작성한 벌크성 수정 쿼리는 @Modifying 어노테이션을 사용하며
벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 어노테이션의 clearAutomatically 옵션을 true로 설정
// JPA를 사용한 벌크성 수정 쿼리
int bulkPriceUp(String stockAmount) {
...
String qlString = "update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString).setParameter("stockAmount", stockAmount).executeUpdate();
}
// 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount";)
int bulkPriceUp(@Param("stockAmount") String stockAmount);
- 반환 타입
스프링 데이터 JPA는 유연한 반환 타입을 지원하는데
결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정
만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환
그리고 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 예외가 발생
참고로 단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메소드를 호출
이 메소드를 호출했을 때 조회 결과가 없으면 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하므로
스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환
List<Member> findByName(String name); // 컬렉션
Member findByEmail(String email); // 단건
- 페이징과 정렬
스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공
파라미터에 Pageable을 사용하면 반환 타입으로 List나 Page를 사용할 수 있으며
반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해
검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출
org.springframework.data.domain.Sort // 정렬 기능
org.springframework.data.domain.Pageable // 페이징 기능 (내부에 Sort 포함)
// 페이징과 정렬 사용 예제
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);
// count 쿼리 사용 안 함
List<Member> findByName(String name, Pageable pageable);
List<Member> findByName(String name, Sort sort;
// 페이징과 정렬을 사용하는 예제
/* 검색 조건 : 이름이 김으로 시작하는 회원
정렬 조건 : 이름으로 내림차순
페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 10건 */
// Page 사용 예제 정의
public interface MemberRepository extends JpaRepository<Member, Long> {
// Pageable은 인터페이스이므로 실제 사용할 때는 해당 인터페이스를 구현한 PageRequest 객체를 사용
Page<Member> findByNameStartingWith(String name, Pageable Pageable);
}
// Page 사용 예제 실행 코드
/* 첫 번째 파라미터는 현재 페이지, 두 번째 파라미터는 조회할 데이터 수를 입력
추가로 정렬 벙보로 파라미터로 사용 */
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Directrion.DESC, "name"));
Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest);
List<Member> members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부
// 반환 타입인 Page 인터페이스가 제공하는 다양한 메소드
public interface Page<T> extends Iterable<T> {
int getNumber(); // 현재 페이지
int getSize(); // 페이지 크기
int getTotalPages(); // 전체 페이지 수
int getNumberOfElements(); // 현재 페이지에 나올 데이터 수
long getTotalElements(); // 전체 데이터 수
boolean hasPreviousPage(); // 이전 페이지 여부
boolean isFistPage(); // 현재 페이지가 첫 페이지인지 여부
boolean hasNextPage(); // 다음 페이지 여부
boolean isLastPage(); // 현재 페이지가 마지막 페이지인지 여부
Pageable nextPageable(); // 다음 페이지 객체, 다음 페이지가 없으면 null
Pageable previousPageable(); // 다음 페이지 객체, 이전 페이지가 없으면 null
List<T> getContent(); // 조회된 데이터
boolean hasContent(); // 조회된 데이터 존재 여부
Sort getSort(); // 정렬 정보
}
- 힌트
JPA 쿼리 힌트를 사용하려면 @QueryHints 어노테이션을 사용하며 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트
forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면
추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션
@QueryHints(value = {@QueryHint(name = "org.hibernate.readOnly", value = "true")},
forCounting = true)
Page<Member> findByName(String name, Pageable pageable);
- Lock
쿼리 시 락을 걸려면 @Lock 어노테이션을 사용하면 됨
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
명세
- 책 도메인 주도 설계는 명세라는 개념을 소개하는데, 스프링 데이터 JPA는 JPA Criteria로 이 개념을 사용할 수 있도록 지원
- 명세를 이해하기 위한 핵심 단어는 술어인데 이것은 단순히 참이나 거짓으로 평가되며 AND, OR 같은 연산자로 조합할 수 있음
그러므로 데이터를 검색하기 위한 계약 조건 하나하나를 술어라 할 수 있으며 Specification 클래스로 술어가 정의됨 - Specification은 컴포지트 패턴으로 구성되어 있어서 여러 Specification을 조합할 수 있어
다양한 검색 조건을 조립해서 새로운 검색 조건을 쉽게 만들 수 있음 - 명세 기능을 사용하려면 JpaSpecificationExecutor 인터페이스를 상속받으면 되며
JpaSpecificaionExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용 - Specification는 명세들을 조립할 수 있도록 도와주는 클래스이며 where(), and(), or(), not() 메소드를 제공
- 명세를 정의하려면 Specification 인터페이스를 구현 (내부 무명 클래스를 사용)
명세를 정의할 때는 toPredicate(...) 메소드만 구현하면 되며
JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어짐
이 파라미터들을 활용해서 적절한 검색 조건을 반환하면 됨
// JpaSpecificationExecutor 상속
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
// JpaSpecificaionExecutor 인터페이스
public interface JpaSpecificaionExecutor<T> {
// JpaSpecificaionExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용
T findOne(Specification<T> spec);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);
}
// 명세 사용 코드
import static org.springframework.data.jpa.domain.Specification.*;
where()
import static jpabook.jpashop.domain.spec.OrderSpec.*;
public List<Order> findOrders(String name) {
// 회원 이름 명세와 주문 상태 명세를 and로 조합해서 검색 조건으로 사용
List<Order> result = orderRepository.findAll(
where(memberName(name)).and(isOrderStatus())
);
return result;
}
// OrderSpec 명세 정의 코드
package jpabook.jpashop.domain;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.*;
public class OrderSpec {
/* 명세를 정의하려면 Specification 인터페이스를 구현 (내부 무명 클래스를 사용)
명세를 정의할 때는 toPredicate(...) 메소드만 구현하면 되며
JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어짐
이 파라미터들을 활용해서 적절한 검색 조건을 반환하면 됨 */
public static Specification<Order> memberNameLike(final String memberName) {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if (StringUtils.isEmpty(memberName)) return null;
Join<Order, Member> m = root.join("member", JoinType.INNER); // 회원과 조인
return builder.like(m.<String>get("name"), "%" + memberName + "%");
}
};
}
public static Specification<Order> orderStatusEq(final OrderStatus orderStatus) {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if (orderStatus == null) return null;
return builder.equal(root.get("status"), orderStatus);
}
};
}
}
사용자 정의 리포지토리 구현
- 스프링 데이터 JPA로 리포지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않지만
다양한 이유로 메소드를 직접 구현해야 할 때도 있는데
그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 하므로
스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공 - 먼저 직접 구현할 메소드를 위한 사용자 정의 인터페이스를 작성한 후, 사용자 정의 인터페이스를 구현한 클래스를 작성
이때 클래스 이름을 짓는 규칙으로는 '리포지토리 인터페이스 이름 + Impl'
이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식하게 됨
마지막으로 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 됨 - 만약 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으면 repository-impl-postfix 속성을 변경
// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
// 사용자 정의 구현 클래스
public class MemberRepositoryImpl implement MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
... // 사용자 정의 구현
}
}
// 사용자 정의 인터페이스 상속
public interface MemberRepository extend JpaRepository<Member, Long>, MemberRepositoryCustom {
}
// 만약 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으
<repositories base-package="jpabook.jpashop.repository"
repository-impl-postfix="Impl" /> // 이 부분을 변경
// JavaConfig 설정
@EnableJpaRepositoroes(basePackages = "jpabook.jpashop.repository", repositoryImplementationPostfix = "Impl")
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션과 영속성 관리 (0) | 2022.05.26 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ③ (0) | 2022.05.18 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ① (0) | 2022.05.16 |
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션 제작 ③ - 애플리케이션 구현 (0) | 2022.05.15 |
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션 제작 ② - 도메인 모델과 테이블 설계 (0) | 2022.05.12 |