이번 시간에 알아볼 것은 querydsl의 QuerydslBinderCustomizer을 알아볼 예정이다.
QuerydslBinderCustomizer는 인터페이스이며 추상 메서드는 void customize(QuerydslBindings bindings, T root) 한개를 갖고 있다.
현재 필자는 회사나 집에서 java8을 쓰기 때문에 java8 기준으로 설명한다
아주 상세하게 컨트롤은 하지 못해도 기본적인 동적쿼리(예로 있으면 검색 아니면 검색하지 않는다)를 간단하게 만들수 있다. 뭐 기능이 얼마나 있는지는 모르겠지만 일단 필자가 테스트한 경우는 기본적인 것만 해봤기 때문에 그것만 설명을 하겠다.
예전에 querydsl를 공부할때 남겨두었던 클래스들을 재 사용했다.
public interface AccountRepository extends QueryDslPredicateExecutor<Account>,
QuerydslBinderCustomizer<QAccount>, JpaRepository<Account, Long>, CustomAccountRepository {
@Override
default void customize(QuerydslBindings bindings, QAccount user) {
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value));
bindings.excluding(user.password);
}
}
여기서 추가 된것은 QueryDslPredicateExecutor 와 QuerydslBinderCustomizer이다. JpaRepository와 CustomAccountRepository는 예전에 공부할때 쓰던거라 일단 남겨두었다.
QueryDslPredicateExecutor 인터페이스는 JpaRepository 와 비슷한 추상 메서드를 갖고 있다. 메서드는 거의 비슷하지만 파라미터가 Predicate predicate 위주로 되어있다.
java8은 인터페이스에서도 구현을 할 수 있어 QuerydslBinderCustomizer의 구현체를 AccountRepository에 만들었다.
구현체에 있는 것들은 실제 request를 보낼때 바인딩되어 그 바인딩된 것으로 쿼리를 하는 것이다. 말로 설명할려니 조금 힘들다. 실제로 테스트를 해보면서 알아가보자.
@RestController
@RequiredArgsConstructor
public class AccountController {
private final AccountRepository accountRepository;
private final ModelMapper modelMapper;
@GetMapping("/accounts")
public Page<AccountDto.Response> accounts(@QuerydslPredicate(root = Account.class) Predicate predicate,
Pageable pageable){
Page<Account> all = accountRepository.findAll(predicate, pageable);
List<AccountDto.Response> collect = all.getContent()
.stream()
.map(i -> modelMapper.map(i, AccountDto.Response.class)).collect(toList());
return new PageImpl<>(collect, pageable, all.getTotalElements());
}
}
어떤한 테스트를 하기 위한 controller이다. 파라미터로는 @QuerydslPredicate(root = Account.class) Predicate 와 Pageable을 받고 있다. 실제로 어떻게 되는지 테스트를 해보자.
http://localhost:8080/accounts
{
"content": [
{
"id": 1,
"name": "wonwoo",
"password": "1PassWord",
"email": "wonwoo@test.com"
},
{
"id": 2,
"name": "wonwoo",
"password": "2PassWord11",
"email": "123@test.com"
},
{
"id": 3,
"name": "kevin",
"password": "3PassWord2",
"email": "aaa@test.com"
},
{
"id": 4,
"name": "ggg",
"password": "PassWord33",
"email": "bbb@test.com"
},
{
"id": 5,
"name": "ggg",
"password": "PassWord44",
"email": "ccc@test.com"
},
{
"id": 6,
"name": "keven",
"password": "PassWord5",
"email": "ddd@test.com"
},
{
"id": 7,
"name": "qqqq",
"password": "PassWord6",
"email": "ggg@test.com"
}
],
"totalElements": 7,
"last": true,
"totalPages": 1,
"size": 20,
"number": 0,
"sort": null,
"first": true,
"numberOfElements": 7
}
파라미터를 아무것도 보내지 않았을 때 이런 결과가 있다고 가정하자.
이번에는 파라미터를 보내보자.
http://localhost:8080/accounts?name=won
{
content: [
{
id: 1,
name: "wonwoo",
password: "1PassWord",
email: "wonwoo@test.com"
},
{
id: 2,
name: "wonwoo",
password: "2PassWord11",
email: "123@test.com"
}
],
totalElements: 2,
last: true,
totalPages: 1,
size: 20,
number: 0,
sort: null,
first: true,
numberOfElements: 2
}
name에 won이 들어가는 것을 모두 가져왔다.
다음에는 password를 검색해보자.
http://localhost:8080/accounts?password=3
{
content: [
{
id: 1,
name: "wonwoo",
password: "1PassWord",
email: "wonwoo@test.com"
},
{
id: 2,
name: "wonwoo",
password: "2PassWord11",
email: "123@test.com"
},
{
id: 3,
name: "kevin",
password: "3PassWord2",
email: "aaa@test.com"
},
{
id: 4,
name: "ggg",
password: "PassWord33",
email: "bbb@test.com"
},
{
id: 5,
name: "ggg",
password: "PassWord44",
email: "ccc@test.com"
},
{
id: 6,
name: "keven",
password: "PassWord5",
email: "ddd@test.com"
},
{
id: 7,
name: "qqqq",
password: "PassWord6",
email: "ggg@test.com"
}
],
totalElements: 7,
last: true,
totalPages: 1,
size: 20,
number: 0,
sort: null,
first: true,
numberOfElements: 7
}
하지만 결과와 다르게 모두 출력 되었다. 그 이유는 위의 코드에서 봤듯이 password는 제외 시켰기 때문이다.
bindings.excluding(user.password);
이번에는 특정 어떠한 파라미터로 오면 그것은 containsIgnoreCase 아닌 eq로 체크 하고 싶다면 아래와 같이 하면 된다.
@Override
default void customize(QuerydslBindings bindings, QAccount user) {
bindings.bind(user.name).first((path, value) -> path.eq(value));
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value));
bindings.excluding(user.password);
}
user.name은 like가 아닌 eq으로 해놨다. 그럼 우리는 정확한 값이 나올때만 출력된다.
아까와 동일하게 http://localhost:8080/accounts?name=won 브라우저에 쳐보자.
{
content: [
],
totalElements: 0,
last: true,
totalPages: 0,
size: 20,
number: 0,
sort: null,
first: true,
numberOfElements: 0
}
그럼 위와 같은 결과를 볼 수 있을 것이다. 그렇다면 이번에는 정확하게 문자를 집어넣어서 해보자.
http://localhost:8080/accounts?name=wonwoo
{
content: [
{
id: 1,
name: "wonwoo",
password: "1PassWord",
email: "wonwoo@test.com"
},
{
id: 2,
name: "wonwoo",
password: "2PassWord11",
email: "123@test.com"
}
],
totalElements: 2,
last: true,
totalPages: 1,
size: 20,
number: 0,
sort: null,
first: true,
numberOfElements: 2
}
그럼 위와 같이 두건이 검색 된다. 우리는 QuerydslBinderCustomizer를 사용해서 좀더 간단하게 코딩을 할 수 있게 되었다. 물론 복잡한 것은 Custom한 레파지토를 만들어 querydsl을 사용하거나 JPQL 혹은 네이티브 SQL을 사용해야 될 것이다. 하지만 저렇게 간단하게 해결 할 수 있는 부분도 물론 있을 것이다.