repository 프록시에는 메소드 이름에서 store별 쿼리를 파생시키는 두 가지 방법이 있습니다.
-
메소드 이름에서 직접 쿼리를 파생합니다.
-
수동으로 정의된 쿼리를 사용합니다.
사용 가능한 옵션은 실제 store에 따라 다릅니다. 그러나 실제 쿼리가 무엇인지 결정하는 전략이 있어야 합니다. 다음 섹션에서는 사용 가능한 옵션에 대해 설명합니다.
Query Lookup Strategies
쿼리를 해결하기 위해 리포지토리 인프라에 다음 전략을 사용할 수 있습니다. XML 구성을 사용하면 query-lookup-strategy
속성(attribute)을 통해 네임스페이스에서 전략을 구성할 수 있습니다. Java 구성의 경우 EnableJpaRepositories
annotation의 queryLookupStrategy
속성(attribute)을 사용할 수 있습니다. 특정 데이터스토어에서는 일부 전략이 지원되지 않을 수 있습니다.
-
CREATE
는 쿼리 메서드 이름에서 store별 쿼리를 생성하려고 시도합니다. 일반적인 접근 방식은 메소드 이름에서 잘 알려진 접두사 세트를 제거하고 메소드의 나머지 부분을 구문 분석하는 것입니다. 쿼리 생성에 대한 자세한 내용은 "쿼리 생성"에서 확인할 수 있습니다.
-
USE_DECLARED_QUERY
는 선언된 쿼리를 찾으려고 시도하고 쿼리를 찾을 수 없으면 예외를 발생시킵니다. 쿼리는 어딘가에 annotation으로 정의되거나 다른 방법으로 선언될 수 있습니다. 해당 store에 사용 가능한 옵션을 찾으려면 해당 store의 설명서를 참조하세요. repository 인프라가 부트스트랩 시 메서드에 대해 선언된 쿼리를 찾지 못하면 실패합니다.
-
CREATE_IF_NOT_FOUND
(기본값)는 CREATE
와 USE_DECLARED_QUERY
를 결합합니다. 선언된 쿼리를 먼저 조회하고, 선언된 쿼리가 없으면 사용자 지정 메서드 이름 기반 쿼리를 생성합니다. 이는 기본 조회 전략이므로 명시적으로 아무것도 구성하지 않은 경우에 사용됩니다. 메소드 이름으로 쿼리를 빠르게 정의할 수 있을 뿐만 아니라 필요에 따라 선언된 쿼리를 도입하여 이러한 쿼리를 사용자 정의할 수도 있습니다.
Query Creation
Spring Data 저장소 인프라에 내장된 쿼리 빌더 메커니즘은 저장소의 엔터티에 대한 제한 쿼리를 작성하는 데 유용합니다. 다음 예에서는 여러 쿼리를 만드는 방법을 보여줍니다.
메소드 이름에서 쿼리 생성
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
List<Person> findByLastnameIgnoreCase(String lastname);
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
파싱 쿼리 메소드 이름은 주어(Subject)와 술어(Predicate)로 구분됩니다. 첫 번째 부분(find…By
, exists…By
)은 쿼리 subject을 정의하고 두 번째 부분은 predicate를 구성합니다. 도입절(subject)에는 추가 표현이 포함될 수 있습니다. find
(또는 기타 도입 키워드)와 By
사이의 모든 텍스트는 생성될 쿼리에 고유한 플래그를 설정하기 위해 Distinct
나 쿼리 결과를 제한하기 위해 Top
/First
와 같은 결과 제한 키워드 중 하나를 사용하지 않는 한 설명적인 것으로 간주됩니다.
부록에는 정렬 및 대/소문자 구분 modifiers를 포함한 쿼리 method predicate 키워드와 쿼리 메소드 subject 키워드의 전체 목록이 포함되어 있습니다. 그러나 첫 번째 By
는 실제 기준 predicate의 시작을 나타내는 구분 기호 역할을 합니다. 매우 기본적인 수준에서는 엔터티 속성(property)에 대한 조건을 정의하고 이를 And
및 Or
로 연결할 수 있습니다.
메서드 구문 분석의 실제 결과는 쿼리를 생성하는 persistence store에 따라 달라집니다. 그러나 주의해야 할 몇 가지 일반적인 사항이 있습니다.
-
표현식은 일반적으로 연결될 수 있는 연산자와 결합된 property traversal입니다. AND
및 OR
를 사용하여 속성(property) 표현식을 결합할 수 있습니다. 속성 표현식에 대해 Between
, LessThan
, GreaterThan
및 Like
와 같은 연산자도 지원됩니다. 지원되는 연산자는 datastore에 따라 다를 수 있으므로 참조 문서의 해당 부분을 참조하세요.
-
메서드 구문 분석기는 개별 속성(예: findByLastnameIgnoreCase(…)
) 또는 대소문자 무시를 지원하는 유형의 모든 속성(일반적으로 String
인스턴스 — 예: findByLastnameAndFirstnameAllIgnoreCase(…)
)에 대해 IgnoreCase
플래그 설정을 지원합니다. 대소문자 무시 지원 여부는 store별로 다를 수 있으므로, store별 쿼리 방법은 참고문서의 해당 부분을 참고하세요.
-
속성(property)을 참조하는 쿼리 메서드에 OrderBy
절을 추가하고 정렬 방향(Asc
또는 Desc
)을 제공하여 정적 순서를 적용할 수 있습니다. 동적 정렬을 지원하는 쿼리 방법을 만들려면 "페이징, 큰 결과 순회, 정렬 및 제한"을 참조하세요.
Property Expressions
속성 식은 앞의 예에 표시된 것처럼 관리 엔터티의 직접 속성만 참조할 수 있습니다. 쿼리 생성 시 구문 분석된 속성이 관리되는 도메인 클래스의 속성인지 이미 확인했습니다. 그러나 중첩된 속성을 탐색하여 제약 조건을 정의할 수도 있습니다. 다음 메서드 서명을 고려하세요.
List<Person> findByAddressZipCode(ZipCode zipCode);
Person
이 ZipCode
가 있는 Address
를 가지고 있다고 가정합니다. 이 경우 메소드는 x.address.zipCode
속성 순회(property traversal)를 생성합니다. resolution 알고리즘은 전체 부분(AddressZipCode
)을 속성으로 해석하는 것으로 시작하고 도메인 클래스에서 해당 이름(대문자화되지 않음)을 가진 속성을 확인합니다. 알고리즘이 성공하면 해당 속성을 사용합니다. 그렇지 않은 경우 알고리즘은 오른쪽의 카멜 케이스 부분에서 소스를 머리와 꼬리로 분할하고 해당 속성(예: AddressZip
및 Code
)을 찾으려고 시도합니다. 알고리즘이 해당 헤드가 있는 속성을 찾으면 꼬리를 가져와서 위에서 설명한 대로 꼬리를 분할하여 계속해서 트리를 만듭니다. 첫 번째 분할이 일치하지 않으면 알고리즘은 분할 지점을 왼쪽(Address
, ZipCode
)으로 이동하고 계속합니다.
이는 대부분의 경우 작동하지만 알고리즘이 잘못된 속성을 선택할 수도 있습니다. Person
클래스에 addressZip
속성도 있다고 가정합니다. 알고리즘은 이미 첫 번째 분할 라운드에서 일치하고 잘못된 속성을 선택하여 실패합니다(addressZip
type에는 code
속성이 없을 수 있으므로).
이 모호함을 해결하려면 메서드 이름 안에 _
를 사용하여 순회 지점을 수동으로 정의할 수 있습니다. 따라서 메소드 이름은 다음과 같습니다.
List<Person> findByAddress_ZipCode(ZipCode zipCode);
밑줄 문자를 예약 문자로 취급하므로 표준 Java 명명 규칙을 따르는 것이 좋습니다(즉, 속성 이름에 밑줄을 사용하지 않고 대신 카멜 표기법을 사용하는 것).
Repository Methods Returning Collections or Iterables
여러 결과를 반환하는 쿼리 메서드는 표준 Java Iterable
, List
및 Set
을 사용할 수 있습니다. 그 외에도 우리는 Iterable
의 사용자 정의 확장인 Spring Data의 Streamable
반환과 Vavr에서 제공하는 컬렉션 유형을 지원합니다. 가능한 모든 쿼리 메소드 반환 유형을 설명하는 부록을 참조하세요.
Using Streamable as Query Method Return Type
Iterable
또는 모든 컬렉션 type 대신 Streamable
을 사용할 수 있습니다. 비병렬 Stream
(Iterable
에는 없음)에 액세스하는 편리한 방법과 요소에 대해 직접 ....filter(...)
및 ....map(...)
을 제공하고 Streamable
을 다른 요소에 연결하는 기능을 제공합니다.
Using Streamable to combine query method results
interface PersonRepository extends Repository<Person, Long> {
Streamable<Person> findByFirstnameContaining(String firstname);
Streamable<Person> findByLastnameContaining(String lastname);
}
Streamable<Person> result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
Returning Custom Streamable Wrapper Types
컬렉션에 대한 전용 래퍼 유형을 제공하는 것은 여러 요소를 반환하는 쿼리 결과에 대한 API를 제공하기 위해 일반적으로 사용되는 패턴입니다. 일반적으로 이러한 유형은 컬렉션과 유사한 유형을 반환하는 저장소 메서드를 호출하고 래퍼 유형의 인스턴스를 수동으로 생성하여 사용됩니다. Spring Data를 사용하면 다음 기준을 충족하는 경우 이러한 래퍼 유형을 쿼리 메서드 반환 유형으로 사용할 수 있으므로 추가 단계를 피할 수 있습니다.
-
type은 Streamable
을 구현합니다.
-
이 type은 생성자 또는 Streamable
을 인수로 사용하는 of(…)
또는 valueOf(…)
라는 정적 팩터리 메서드를 노출합니다.
다음 목록은 예를 보여줍니다.
class Product {
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {
private final Streamable<Product> streamable;
public MonetaryAmount getTotal() {
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator<Product> iterator() {
return streamable.iterator();
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text);
}
(1) 제품 가격에 액세스하기 위해 API를 노출하는 Product
엔터티입니다.
(2) Products.of(…)
(Lombok annotation으로 생성된 팩토리 메서드)를 사용하여 생성할 수 있는 Streamable<Product>
에 대한 래퍼 type입니다. Streamable<Product>
를 취하는 표준 생성자도 마찬가지입니다.
(3) 래퍼 type은 Streamable<Product>
에서 새 값을 계산하는 추가 API를 노출합니다.
(4) Streamable
인터페이스를 구현하고 실제 결과에 위임합니다.
(5) 해당 래퍼 유형 Products
은 쿼리 메서드 반환 유형으로 직접 사용할 수 있습니다. Streamable<Product>
를 반환하고 리포지토리 클라이언트에서 쿼리 후에 수동으로 래핑할 필요가 없습니다.
Support for Vavr Collections
Vavr은 Java의 함수형 프로그래밍 개념을 수용하는 라이브러리입니다. 다음 표에 표시된 것처럼 쿼리 메서드 반환 유형으로 사용할 수 있는 사용자 지정 컬렉션 유형 집합이 함께 제공됩니다
Vavr 컬렉션 유형 |
사용된 Vavr 구현 유형 |
유효한 Java 소스 유형 |
io.vavr.collection.Seq
|
io.vavr.collection.List
|
java.util.Iterable
|
io.vavr.collection.Set
|
io.vavr.collection.LinkedHashSet
|
java.util.Iterable
|
io.vavr.collection.Map
|
io.vavr.collection.LinkedHashMap
|
java.util.Map
|
실제 쿼리 결과(세 번째 열)의 Java 유형에 따라 첫 번째 열의 유형(또는 그 하위 유형)을 쿼리 메소드 반환 유형으로 사용하고 두 번째 열의 유형을 구현 유형으로 가져올 수 있습니다. 또는 Traversable
(Vavr Iterable
과 동일)을 선언한 다음 실제 반환 값에서 구현 클래스를 파생시킬 수 있습니다. 즉, java.util.List
는 Vavr List
또는 Seq
로 바뀌고, java.util.Set
은 Vavr LinkedHashSet
Set
이 되는 식입니다.
Streaming Query Results
Java 8 Stream<T>
을 반환 유형으로 사용하여 쿼리 메서드의 결과를 증분적으로 처리할 수 있습니다. 쿼리 결과를 Stream
으로 래핑하는 대신 다음 예제와 같이 데이터 저장소별 메서드를 사용하여 스트리밍을 수행합니다.
Java 8 Stream<T>
을 사용하여 쿼리 결과 스트리밍
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();
Stream<User> readAllByFirstnameNotNull();
@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
[Note]
Stream
은 잠재적으로 기본 데이터 저장소별 리소스를 래핑하므로 사용 후에는 닫혀야 합니다. 다음 예제와 같이 close()
메서드를 사용하거나 Java 7 try-with-resources
블록을 사용하여 Stream
을 수동으로 닫을 수 있습니다.
Stream<T>
로 작업하면 try-with-resources
블록이 발생합니다.
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
[Note]
현재 모든 Spring Data 모듈이 Stream<T>
을 반환 유형으로 지원하는 것은 아닙니다.
Asynchronous Query Results
Spring의 비동기 메소드 실행 기능을 사용하여 저장소 쿼리를 비동기적으로 실행할 수 있습니다. 이는 Spring TaskExecutor
에 제출된 작업에서 실제 쿼리가 발생하는 동안 메서드가 호출 즉시 반환됨을 의미합니다. 비동기 쿼리는 반응 쿼리와 다르므로 혼합되어서는 안 됩니다. 대응적 지원에 대한 자세한 내용은 매장별 설명서를 참조하세요. 다음 예에서는 다양한 비동기 쿼리를 보여줍니다.
@Async
Future<User> findByFirstname(String firstname);
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
(1) 반환 유형으로 java.util.concurrent.Future
를 사용합니다.
(2) 반환 유형으로 Java 8 java.util.concurrent.CompletableFuture
를 사용합니다.
Paging, Iterating Large Results, Sorting & Limiting
쿼리의 매개변수를 처리하려면 이전 예제에서 이미 본 것처럼 메서드 매개변수를 정의합니다. 그 외에도 인프라는 Pageable
, Sort
및 Limit
과 같은 특정 type을 인식하여 쿼리에 페이지 매김, 정렬 및 제한을 동적으로 적용합니다. 다음 예에서는 이러한 기능을 보여줍니다.
쿼리 메서드에서 Pageable, Slice, Sort 및 Limit 사용
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Sort sort, Limit limit);
List<User> findByLastname(String lastname, Pageable pageable);
[Important]
Sort
, Pageable
및 Limit
을 사용하는 API는 null
이 아닌 값이 메서드에 전달될 것으로 예상합니다. 정렬이나 페이지 매김을 적용하지 않으려면 Sort.unsorted()
, Pageable.unpaged()
및 Limit.unlimited()
를 사용하세요.
첫 번째 방법을 사용하면 org.springframework.data.domain.Pageable
인스턴스를 쿼리 메서드에 전달하여 정적으로 정의된 쿼리에 페이징을 동적으로 추가할 수 있습니다. Page
는 사용 가능한 총 요소 및 페이지 수를 알고 있습니다. 전체 숫자를 계산하기 위해 개수 쿼리를 트리거하는 인프라를 통해 이를 수행합니다. 비용이 많이 들 수 있으므로(사용된 저장소에 따라) 대신 Slice
를 반환할 수 있습니다. Slice
는 다음 Slice
가 사용 가능한지 여부만 알고 있으며 이는 더 큰 결과 세트를 탐색할 때 충분할 수 있습니다.
정렬 옵션도 Pageable
인스턴스를 통해 처리됩니다. 정렬만 필요한 경우 org.springframework.data.domain.Sort
매개변수를 메서드에 추가하세요. 보시다시피 List
를 반환하는 것도 가능합니다. 이 경우 실제 Page
인스턴스를 구축하는 데 필요한 추가 메타데이터가 생성되지 않습니다(즉, 필요했던 추가 개수 쿼리가 실행되지 않음을 의미). 오히려 지정된 엔터티 범위만 조회하도록 쿼리를 제한합니다.
[Note]
전체 쿼리에 대해 얻은 페이지 수를 확인하려면 추가 개수 쿼리를 실행해야 합니다. 기본적으로 이 쿼리는 실제로 트리거한 쿼리에서 파생됩니다.
[Important]
특수 매개변수는 쿼리 메소드 내에서 한 번만 사용할 수 있습니다.
위에 설명된 일부 특수 매개변수는 상호 배타적입니다. 다음의 잘못된 매개변수 조합 목록을 고려하세요.
매개변수 |
예시 |
이유 |
Pageable 및 Sort
|
findBy…(Pageable page, Sort sort)
|
Pageable 은 이미 Sort 를 정의하고 있습니다.
|
Pageable 및 Limit
|
findBy…(Pageable page, Limit limit)
|
Pageable 은 이미 한도를 정의하고 있습니다.
|
결과를 제한하는 데 사용되는 Top
키워드는 Pageable
과 함께 사용할 수 있는 반면 Top
은 결과의 총 최대값을 정의하는 반면 Pageable 매개변수는 이 숫자를 줄일 수 있습니다.
Which Method is Appropriate?
Spring Data 추상화에서 제공하는 값은 아마도 아래 표에 설명된 가능한 쿼리 메서드 반환 유형으로 가장 잘 표시될 것입니다. 표에는 쿼리 메서드에서 반환할 수 있는 유형이 나와 있습니다.
Table 1. Consuming Large Query Results
표 1. 대용량 쿼리 결과 소비
방법 |
데이터 검색 양 |
쿼리 구조 |
제약 사항 |
List<T>
|
모든 결과. |
단일 쿼리. |
쿼리 결과가 모든 메모리를 소진할 수 있습니다. 모든 데이터를 검색하는 데 시간이 오래 걸릴 수 있습니다. |
Streamable<T>
|
모든 결과. |
단일 쿼리. |
쿼리 결과가 모든 메모리를 소진할 수 있습니다. 모든 데이터를 검색하는 데 시간이 오래 걸릴 수 있습니다. |
Stream<T>
|
체크된 (하나씩 또는 일괄 처리로) Stream 소비에 따라 청크화됩니다. |
일반적으로 커서를 사용한 단일 쿼리. |
리소스 누수를 피하기 위해 사용 후 스트림을 닫아야 합니다. |
Flux<T>
|
체크된 (하나씩 또는 일괄 처리로) Flux 소비에 따라 청크화됩니다. |
일반적으로 커서를 사용한 단일 쿼리. |
저장 모듈이 반응형 인프라를 제공해야 합니다. |
Slice<T>
|
Pageable.getOffset() 에서 시작하는 데이터 검색 양은 Pageable.getPageSize() + 1 입니다.
|
Pageable.getOffset() 에서 시작하는 데이터를 검색하는 일대다 쿼리를 적용하여 데이터를 검색합니다.
|
Slice 는 다음 Slice 로 이동할 수 있습니다.
-
Slice 는 더 많은 데이터를 검색할 수 있는지에 대한 세부 정보를 제공합니다.
-
오프셋 기반 쿼리는 오프셋이 너무 큰 경우 효율이 떨어집니다. 왜냐하면 데이터베이스는 여전히 전체 결과를 실체화해야 하기 때문입니다.
-
Window 는 더 많은 데이터를 검색할 수 있는지에 대한 세부 정보를 제공합니다.
-
오프셋 기반 쿼리는 오프셋이 너무 큰 경우 효율이 떨어집니다. 왜냐하면 데이터베이스는 여전히 전체 결과를 실체화해야 하기 때문입니다.
|
Page<T>
|
Pageable.getOffset() 에서 시작하는 데이터 검색 양은 Pageable.getPageSize() 입니다.
|
Pageable.getOffset() 에서 시작하는 일대다 쿼리를 적용하여 데이터를 검색합니다. 추가로, 총 요소 수를 결정하기 위해 COUNT(…) 쿼리가 필요할 수 있습니다.
|
자주 COUNT(…) 쿼리가 필요하며 이는 비용이 많이 듭니다.
|
Paging and Sorting
속성 이름을 사용하여 간단한 정렬 표현식을 정의할 수 있습니다. 표현식을 연결하여 여러 기준을 하나의 표현식으로 수집할 수 있습니다.
Defining sort expressions
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
보다 형식에 안전한 정렬 식을 정의하려면 정렬 식을 정의할 형식부터 시작하고 메서드 참조를 사용하여 정렬할 속성을 정의하세요.
유형이 안전한 API를 사용하여 정렬 표현식 정의
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
[Note]
TypedSort.by(…)
는 (일반적으로) CGlib를 사용하여 런타임 프록시를 사용합니다. 이는 Graal VM Native와 같은 도구를 사용할 때 기본 이미지 컴파일을 방해할 수 있습니다.
저장소 구현이 Querydsl을 지원하는 경우 생성된 메타모델 유형을 사용하여 정렬 표현식을 정의할 수도 있습니다.
Defining sort expressions by using the Querydsl API
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
Limiting Query Results
페이징 외에도 전용 Limit
매개변수를 사용하여 결과 크기를 제한할 수 있습니다. 또한 First
또는 Top
키워드를 사용하여 쿼리 메서드의 결과를 제한할 수도 있습니다. 이 키워드는 서로 바꿔서 사용할 수 있지만 Limit
매개 변수와 혼합할 수는 없습니다. Top
또는 First
에 선택적 숫자 값을 추가하여 반환할 최대 결과 크기를 지정할 수 있습니다. 숫자를 생략하면 결과 크기가 1로 가정됩니다. 다음 예에서는 쿼리 크기를 제한하는 방법을 보여줍니다.
Top 및 First를 사용하여 쿼리 결과 크기 제한
List<User> findByLastname(Limit limit);
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
제한 표현식은 고유 쿼리를 지원하는 데이터 저장소에 대해 Distinct
키워드도 지원합니다. 또한 결과 집합을 하나의 인스턴스로 제한하는 쿼리의 경우 Optional
키워드를 사용하여 결과를 래핑하는 것이 지원됩니다.
제한된 쿼리 페이지네이션(및 사용 가능한 페이지 수 계산)에 페이지네이션 또는 슬라이싱을 적용하는 경우 제한된 결과 내에서 적용됩니다.
[Note]
Sort
매개변수를 사용하여 동적 정렬과 함께 결과를 제한하면 'K' 가장 작은 요소뿐만 아니라 'K' 가장 큰 요소에 대한 쿼리 방법을 표현할 수 있습니다.