Spring Data Commons 모듈에 있는 Example<T> 인터페이스를 이용하면 간단한 동적쿼리 작성이 가능하다.
Criteria API나 QueryDSL에 비하면 기능이 부족하다고 느껴지지만,
별도의 의존성을 추가할 필요가 없고 특정 인터페이스를 상속할 필요도 없기때문에 간단한 쿼리 작성에는 사용해볼 가치가 있다.
Query By Example API는 4가지의 파트로 구성된다.
- Probe : 기준이 되는 필드로 구성된 도메인 객체 예시
ExampleMatcher: 특정 필드들에 대한 조건을 작성Example: Probe와ExampleMatcher를 포함하며, 실제 쿼리 생성에 사용FetchableFluentQuery: Fluent API 관련
기본적인 사용방식은 아래와 같다.
Person probe = new Person();
probe.setFirstname("f"); // 1
ExampleMatcher eMatcher = ExampleMatcher.matching()
.withIgnorePaths("lastname")
.withMatcher("firstname", startsWith().ignoreCase()); // 2
Example<Person> example = Example.of(probe, eMatcher); // 3
List<Person> pList = personRepository.findAll(example); // 4
ExampleMatcher 생성ExampleMatcher를 이용하여 Example<T> 생성Example<T>를 쿼리메서드의 매개변수로 전달예를들어, 위 코드는 firstname이 f로 시작하는 like 쿼리를 생성해낼 것이다.
.withIgnorePaths("lastname") 이 부분은 where 절에서 조건판단 시에 lastname 속성을 무시한다는 내용이다.
( 하지만 위 코드에서는 무의미한 작성이므로 없어도 된다 )
실제로 생성되는 쿼리를 살펴보자

예상했던대로 like 쿼리가 생성되는 것을 확인할 수 있다.
Example<T>생성 시,ExampleMatcher는 선택적으로 제공할 수 있다.
만약,ExampleMatcher를 제공하지 않는다면 Probe 객체와의=연산이 이뤄진다.
만약, Repository에
JpaRepository가 아니라CrudRepository등을 상속했다면 QBE가 동작하지 않을 수 있다.
이 경우에는QueryByExampleExecutor를 상속해줘야한다.다른말로 표현하자면 해당 인터페이스에 있는 쿼리메서드들을 QBE에 사용할 수 있다
ExampleMatcher는 where 절 조건에 해당되는 내용을 작성할 수 있으며, 그 분류는 아래와 같다
- null 핸들링( ex :
.withIncludeNullValues)- 문자열 매칭( ex :
.withStringMatcher)- 특정 속성 무시( ex :
.withignorePaths)- 대소문자 조건( ex :
.withignoreCase)- 값 변환 ( ex :
.withTransformer)좀 더 자세한 내용은 공식 문서를 참고하자
하단의
public interface QueryByExampleExecutor<T> {
// ...
<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}
QueryByExampleExecutor<T> 인터페이스의 findBy 메서드를 살펴보자
해당 메서드의 두번째 매개변수인 queryFunction을 이용하면 Example 만으로 설정가능한 조건외의 추가적인 조건들을 설정할 수 있다.
sortBy를 이용하면 정렬기준을 설정할 수 있고,
as를 이용하면 결과 반환 유형을 지정할 수 있으며,
project를 이용하면 projection을 적용할 수 있다.
log.info("QBE START");
Person probe = new Person();
probe.setFirstname("f");
ExampleMatcher eMatcher = ExampleMatcher.matching()
.withMatcher("firstname", startsWith().ignoreCase());
Example<Person> example = Example.of(probe, eMatcher);
List<Person> pList = personRepository.findBy(example, q -> q
.project("lastname")
.sortBy(Sort.by("lastname").descending())
.all()
);
pList.forEach(p -> log.info(p.toString()));
log.info("QBE END");
위 코드처럼 람다형식으로 findBy를 호출할 때 상세조건을 작성할 수 있다.
다만,
project의 경우 예상대로 동작하지 않는 문제가 있다.
파생되는 쿼리에서lastname속성만 조회할 것이라는 예상과 다르게 실제로 파생된 쿼리는 아래와 같았다.select p1_0.id,p1_0.city,p1_0.street,p1_0.zip_code,p1_0.firstname,p1_0.lastname from person p1_0 where lower(p1_0.firstname) like ? escape '\\' order by p1_0.lastname desc즉, 모든 속성값을 읽어오는 쿼리가 파생되었다