조건은 다음과 같습니다.
검색 조건
- 검색 키워드로 일정의 제목을 검색이 가능해야 한다.
- 제목은 부분적으로 일치해도 검색이 가능해야 한다.
- 일정의 생성일 범위로 검색할 수 있어야 한다.
- 일정을 생성일 최신순으로 정렬이 가능해야 한다.
- 담당자의 닉네임으로도 검색이 가능해요.
- 닉네임은 부분적으로 일치해도 검색이 가능해야한다.
- 다음의 내용을 포함해서 검색 결과를 반환해야한다.
- 일정에 대한 모든 정보가 아닌, 제목만 넣도록 해야 한다.
- 해당 일정의 담당자 카운트가 가능해야 한다.
- 해당 일정의 총 댓글 카운트가 가능해야 한다.
- 검색 결과는 페이징 처리.
QueryDSL 구현순서
1. Entity 작성
이미 comment와 user간의 연관관계가 이어져 있기에 추가적으로 수정할 필요는 없었다.
package org.example.expert.domain.todo.entity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.expert.domain.comment.entity.Comment;
import org.example.expert.domain.common.entity.Timestamped;
import org.example.expert.domain.manager.entity.Manager;
import org.example.expert.domain.user.entity.User;
import java.util.ArrayList;
import java.util.List;
@Getter
@Entity
@NoArgsConstructor
@Table(name = "todos")
public class Todo extends Timestamped {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String contents;
private String weather;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE)
private List<Comment> comments = new ArrayList<>();
@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();
public Todo(String title, String contents, String weather, User user) {
this.title = title;
this.contents = contents;
this.weather = weather;
this.user = user;
this.managers.add(new Manager(user, this));
}
}
2. DTO 클래스 작성
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TodoSearchResponse {
private String title; // 일정 제목
private Long userCount; // 담당자 수
private Long commentCount; // 댓글 수
}
TodoSearchResponse를 통해 API 응답에서 필요한 필드(제목, 담당자 수, 댓글 수)만 반환합니다.
3. Repository 수정
TodoRepsoitory에 QuerydslPredicateExecutor를 추가합니다.
여기서 QuerydslPredicateExecutor는 어떤 기능을 수행하냐면 확장 기능이며 Querydsl의 Predicate 기능을 활용할 수 있습니다.
-여기서 Predicate란 QueryDSL이나 Criteria API에서 조건을 표현할 때 사용되는 개념이다. 그래서 Predicate를 사용하면 쿼리에서 Where절을 좀 더 간편하기 가독성 있게 작성이 가능하다.(예를 들어 AND, OR, IN, LIKE 등과 같이 SQL 조건을 Predicate로 작성하여 필터링 조건을 정의한다.)
그래서 이를 통해 동적 쿼리를 보다 유연하게 작성하고 활용이 가능합니다. 그래서 Repository를 별도로 만드는 것이 아니라 기존의 TodoRepository를 확장하여 QuerydslPredicateExecutor를 포함시키는 방식으로 활용합니다.
4. Service 작성
@Transactional(readOnly = true)
public Page<TodoSearchResponse> searchTodos(String keyword, LocalDateTime startDate, LocalDateTime endDate, String nickname, Pageable pageable) {
QTodo todo = QTodo.todo;
QComment comment = QComment.comment;
List<TodoSearchResponse> results = jpaQueryFactory
.select(Projections.constructor(TodoSearchResponse.class,
todo.title,
todo.user.count(),
comment.count()
))
.from(todo)
.leftJoin(todo.user)
.leftJoin(todo.comments, comment)
.where(
todo.title.containsIgnoreCase(keyword)
.or(todo.user.nickname.containsIgnoreCase(nickname))
.and(todo.createdAt.between(startDate, endDate))
)
.groupBy(todo.id)
.orderBy(todo.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = jpaQueryFactory
.select(todo.count())
.from(todo)
.where(
todo.title.containsIgnoreCase(keyword)
.or(todo.user.nickname.containsIgnoreCase(nickname))
.and(todo.createdAt.between(startDate, endDate))
)
.fetchOne();
return new PageImpl<>(results, pageable, total);
}
일단 get메서드로 호출하기 때문에 읽기 전용으로 해야 되기 때문에 @Transactional(readOnly = true)하여 데이터 베이스 수정 없이 검색 작업만 수행하도록 하였습니다.
QueryDSL 객체 초기화
QTodo와 QComment 객체를 초기화하여 쿼리 작성에 준비합니다.
QTodo와 QComment는 QueryDSL이 생성한 엔티티의 메타데이터 클래스입니다.
- 메타데이터(Metadata): 데이터에 관한 정보를 설명하는 데이터의 데이터를 의미한다. 쉽게 말해, 메타데이터는 특정 데이터의 구조, 속성, 내용, 또는 관리 방법에 대한 정보를 제공하여 해당 데이터를 정의하는 역할을 한다.
검색 조건 및 결과 조회
jpaQueryFactory를 사용하여 select 쿼리를 작성하고, Projections.construtor를 통해 TodoSearchResponse 객체를 생성합니다.
jpaQueryFactory를 사용하여 select 쿼리를 작성하고, Projections.constructor를 통해 TodoSearchResponse 객체를 생성합니다.
쿼리에서 다음과 같은 필드를 조회합니다
- todo.title: Todo 엔티티의 제목.
- todo.user.count(): Todo의 담당자 수 (담당자 수를 집계).
- comment.count(): Todo의 총 댓글 수 (댓글 수를 집계).
- Projections.construtor가 하는 역할은?
쿼리의 결과를 특정 클래스의 생성자를 이용해 객체로 매핑할 때 사용된다. Projections 클래스를 활용하면, SQL 쿼리의 결과를 원하는 DTO (Data Transfer Object)나 특정 클래스 형태로 변환하여 반환할 수 있다.
- DTO 클래스로 매핑: 엔티티 전체를 반환하는 것이 아니라, 쿼리 결과를 DTO 클래스로 변환하여 필요한 정보만 가져오는 데 사용.
- 필요한 필드만 반환: 엔티티의 특정 필드만 가져오거나 여러 테이블의 조인 결과를 조합하여 반환할 때 유용.
- 타입 안전성 보장: 필드와 생성자 파라미터 간의 타입 매칭을 보장하여 컴파일 시점에 타입 오류를 방지할 수 있다.
JOIN 및 조건 설정
leftJoin(todo.user)와 leftJoin(todo.comments, comment): Todo와 User, Comment를 조인하여, 필요한 데이터를 결합합니다.
where 절에서는 다음과 같은 조건을 설정하여 동적으로 데이터를 필터링합니다
- todo.title.containsIgnoreCase(keyword): 제목에 검색 키워드가 포함된 경우.
- todo.user.nickname.containsIgnoreCase(nickname): 담당자의 닉네임에 검색 키워드가 포함된 경우.
- todo.createdAt.between(startDate, endDate): 생성일이 startDate와 endDate 사이에 있는 경우.
페이징 처리
offset(pageable.getOffset())와 limit(pageable.getPageSize())를 사용하여 페이지의 시작 위치와 크기를 설정합니다.
전체 결과 개수 조회
ong total = jpaQueryFactory.select(todo.count()).from(todo).where(...)와 같은 형태로 Todo 항목의 전체 개수를 조회하여 페이징 정보에 사용합니다.
결과 반환
PageImpl 객체로 조회된 결과 리스트(results), 페이징 정보(pageable), 전체 개수(total)를 묶어 반환합니다.
해당 메소드의 전체 흐름을 보자면...
이 메소드는 입력 받은 검색 조건(keyword, startDate, endDate, nickname)을 기반으로 Todo항목을 조회하고, 필요한 필드(title, user count, comment count)만 반환하도록 최적화된 쿼리를 작성합니다. 또한, 검색된 데이터를 처리하여 원하는 페이지의 결과만을 반환하고, 전체 결과 개수도 함께 반환하여 페이징 정보가 포함된 Page 객체를 반환합니다.
+ 예외 처리중 특정 조건이 null일 때(keyword) 해당 조건을 무시하도록 Predicate를 활용하여 조건을 동적으로 추가적으로 구성해줘야한다.
Controller 작성
API의 Endpoint를 설정하는 Controller를 작성합니다.
@GetMapping("/todos/search")
public ResponseEntity<Page<TodoSearchResponse>> SearchTodos(
@RequestParam String keyword,
@RequestParam LocalDateTime startDate,
@RequestParam LocalDateTime endDate,
@RequestParam(required = false) String nickname,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<TodoSearchResponse> result = todoService.searchTodos(keyword, startDate, endDate, nickname, pageable);
return ResponseEntity.ok(result);
}
이 컨트롤러를 통해 클라이언트로부터 일정 검색 요청을 받고, 주어진 조건에 따라 일정을 검색한 후, 그 결과를 페이지 형식으로 반환합니다.
'Spring > QueryDSL, JPQL' 카테고리의 다른 글
QueryDSL (2) | 2024.10.05 |
---|---|
JPQL (4) | 2024.10.02 |