[내일배움캠프Spring-57일차] Query DSL 로 검색기능을 구현해보자 !
이전 포스팅에서 Query DSL 를 적용해본적이 있다.
우리는 그 Query DSL 를 통해서 한번 검색 기능을 만들어볼 것이다.
1. 요구사항
- 먼저 요구사항부터 확인해보자 !
① 검색 키워드로 일정의 제목을 검색할 수 있어요.
- 제목은 부분적으로 일치해도 검색이 가능해요.
② 일정의 생성일 범위로 검색할 수 있어요.
- 일정을 생성일 최신순으로 정렬해주세요.
③ 담당자의 닉네임으로도 검색이 가능해요.
- 닉네임은 부분적으로 일치해도 검색이 가능해요.
④ 반환 값을 아래와 같이 해야한다.
- 일정에 대한 모든 정보가 아닌, 제목만 넣어주세요.
- 해당 일정의 담당자 수를 넣어주세요.
- 해당 일정의 총 댓글 개수를 넣어주세요.
2. 컨트롤러부터 해보자
@GetMapping("/todos/search")
public ResponseEntity<Page<TodoSearchResponse>> searchTodos(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "") String title,
@RequestParam(defaultValue = "1000-01-01") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam(defaultValue = "2999-12-31") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate,
@RequestParam(defaultValue = "") String nickname) {
return ResponseEntity.ok(todoService.searchTodos(page, size, title, startDate, endDate, nickname));
}
- 페이징 처리가 되어야하기 때문에, page 와 size 도 Param 으로 받았다.
- null 값을 서비스단에 넘기기 싫어서, defaultValue 를 빈 값으로 넘겨주기로 했다.
- 일정의 생성 범위로 하라고 해달라기 했기 때문에, Start Date 와 End Date 를 두 가지 받아올 것이다.
- 둘다 Default 로 주었는데, 이렇게 해두면 별다른 조건을 주지 않아도 처음시점부터 원하는 시점까지의 일정을 받아올 수 있을 것이다 !
3. 서비스
public Page<TodoSearchResponse> searchTodos(int page, int size, String title, LocalDate startDate, LocalDate endDate, String nickname) {
// 1. 시간을 알맞게 형태에 맞추기
LocalDateTime startDateTime = startDate.atStartOfDay();
LocalDateTime endDateTime = endDate.atTime(23, 59, 59, 999_999_999);
// 2. Pageable 객체 만들기
Pageable pageable = PageRequest.of(page - 1, size);
// 3. 조회하기
return todoCustomRepository.searchTodos(title, startDateTime, endDateTime, nickname, pageable);
}
- 우리가 처음 받았을 때는 LocalDate 였지만, 실제 DB 의 created 열이 LocalDateTime 이기 때문에 형태를 맞추어 줘야한다 !
- Pageable 객체도 만들어준다.
4. Repository - QueryDSL
- 일단 QueryDsl 같은 경우는 페이징 객체가 지원이 되지 않는다.
- 그리하여 우리는 직접 페이징을 구현해주어야하는데. 그럴려면 countQuery 라는
public Page<TodoSearchResponse> searchTodos(String title, LocalDateTime startDateTime, LocalDateTime endDateTime, String nickname, Pageable pageable) {
JPAQuery<Long> countQuery = queryFactory
.select(todo.count())
.from(todo)
.where(titleContains(title),
nicknameContains(nickname),
todo.createdAt.between(startDateTime, endDateTime));
List<TodoSearchResponse> content = queryFactory
.select(new QTodoSearchResponse(
todo.title,
todo.managers.size(),
todo.comments.size()
))
.from(todo)
.where(titleContains(title),
nicknameContains(nickname),
todo.createdAt.between(startDateTime, endDateTime))
.orderBy(todo.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchOne());
}
private BooleanExpression titleContains(String title) {
return hasText(title) ? todo.title.contains(title) : null;
}
private BooleanExpression nicknameContains(String nickname) {
return hasText(nickname) ? todo.user.nickname.contains(nickname) : null;
}
- Spring Data의 Page<T> 객체를 만들려면 다음 정보가 필요하다.
- 현재 페이지에 담길 데이터 목록 (content)
- 전체 데이터의 총 개수 (total count)
따라서
- Pageable을 리턴하려면 전체 개수(total count)가 꼭 필요함.
- fetch()로는 리스트만 가져오므로 개수는 별도로 구해야 함.
- 그래서 .count() 쿼리를 따로 만들어 PageImpl 생성 시 넣어주는 것.
- 그리하여 우리는 CountQuery 와 Content Query 를 만들어 주어야 하는 것이다 !
- 여기서 한 부분 조금 짚고 가고 싶은 부분이 있는데, Query DSL 같은 경우에는 where 절에 null 이 들어가면 해당 조건은 무시한다는 것이다 ! 맨 아래 코드가 BooleanExpression 메서드 들을 담아낸 것이다 !
- SQL 문에서 If 문과 같다고 ? 생각하면 좋을 거 같다.
- 그리하여 아래에서 BooleanExpression 를 반환했는데. QueryDSL에서 WHERE 절 조건을 조립할 때 사용하는 불리언 타입의 표현식 객체이다. 쉽게 말하면, "이 조건 맞아?"를 표현하는 객체 이다. 그러니까 우리가 where 절에 넣는 거 조건들 BooleanExpression 객체 이라는 것이다. (todo.title.contains()) <- 이게 반환되는게. 이 객체가 ! 바로 BooleanExpression )
- 이러면 QueryDSL 을 통한 검색기능 구현 완료 : > 근데 과연 이게 효율적인 ? Search 문인지는 아직 의문이 든다 .. !
5. 실행 결과
> 제목이랑 nickname 을 안 넣었을 경우
> 제목이랑 nickname 을 넣었을 경우