- entity 전체의 값을 가져오는 것이 아닌 조회 대상을 지정해 원하는 값만 조회할 수 있도록 지원하는 클래스
- DB조회 시 select * from table 간에 사용되는 * 가 모든 필드를 가져오므로, 불필요한 메모리 낭비가 될 수 있음.
Projections 사용법 4가지
- bean: setter로 주입하며, 필드명이 일치해야함
- field: setter 없이 주입 가능하며, 필드명이 일치해야함
- constructor
- 생성자 기반으로 삽입하여, 네이밍 상관 없이 생성자 필드순으로 바인딩
- 생성자에 입력한 값 순서로 확인하기 때문에 컴파일 시점에 오류 확인이 불가하여, 런타임 간에 발견될 수 있음
-
- 바인딩 DTO에 @QueryProjection가 있으면 컴파일 시점에 entity와 마찬가지로 Q클래스를 생성
- 미리 생성된 Q클래스에 의해 컴파일 시점에 오류 확인 가능
- QueryDSL을 사용하기 위한 클래스를 별도 관리 필요annotation
constuctor 방식
TodoProjectionDto
@Getter
@RequiredArgsConstructor
public class TodoProjectionDto {
private final String title;
private final int mamnagerCount;
private final int commentCount;
출력하고자 하는 데이터 형식 Dto 를 만들어준다.
TodoQueryRepositoryImpl
.select(
Projections.constructor(
TodoProjectionDto.class,
todo.title, // 할일 제목
todo.managers.size(), // 담당자 수
todo.comments.size() // 댓글 수
)
)
Projections.constructor 을 이용해서 위와 같이 필요한 정보들을 매칭시켜준다.
@annotation 방식
TodoProjectionDto
@Getter
public class TodoProjectionDto {
private final String title;
private final int mamnagerCount;
private final int commentCount;
@QueryProjection
public TodoProjectionDto(String title, int mamnagerCount, int commentCount) {
this.title = title;
this.mamnagerCount = mamnagerCount;
this.commentCount = commentCount;
}
}
생성자 앞에 @QueryProjection 어노테이션을 달아주고 build 를 해서 Q클래스를 만들어준다.
그럼 위와 같이 Q 클래스가 나온 것을 볼 수 있다.
TodoQueryRepositoryImpl
.select(
new QTodoProjectionDto(
todo.title, // 할일 제목
todo.managers.size(), // 담당자 수
todo.comments.size() // 댓글 수
)
)
그런 다음 QTodoProjectionDto 를 이용해서 매칭시켜준다.
예시코드2
@Override
public Page<TodoProjectionDto> findByIdFromProjection(String title, String nickname, LocalDate start, LocalDate end, Pageable pageable) {
BooleanBuilder builder = new BooleanBuilder();
// 제목 검색
if (title != null && !title.isEmpty()) {
builder.and(titleContains(title));
}
// 닉네임 검색
if (nickname != null && !nickname.isEmpty()) {
builder.and(nicknameContains(nickname));
}
// 생성일 범위 검색
if (start != null && end != null) {
builder.and(createdDateBetween(start, end));
} else if (start != null) {
builder.and(createdDateAfter(start));
} else if (end != null) {
builder.and(createdDateBefore(end));
}
List<TodoProjectionDto> content = queryFactory
.select(
new QTodoProjectionDto(
todo.title, // 할일 제목
todo.managers.size(), // 담당자 수
todo.comments.size() // 댓글 수
)
)
.from(todo)
.leftJoin(todo.managers)
.leftJoin(todo.comments)
.where(builder)
.orderBy(todo.createdAt.desc()) // 생성일 최신순 조회
.offset(pageable.getOffset()) // 페이지 넘버
.limit(pageable.getPageSize()) // 페이지 크기 ( 페이지당 할일 표시 수 )
.fetch();
long total = Optional.ofNullable(queryFactory
.select(todo.count())
.from(todo)
.where(builder)
.fetchOne())
.orElse(0L);
return new PageImpl<>(content, pageable, total);
}
// todoId 로 조회 메서드
private BooleanExpression todoIdEq(Long todoId) {
return todoId != null ? todo.id.eq(todoId) : null;
}
// 제목 검색 포함 조회 메서드
private BooleanExpression titleContains(String title) {
return title != null ? todo.title.contains(title) : null ;
}
// 닉네임 검색 포함 조회 메서드
private BooleanExpression nicknameContains(String nickname) {
return nickname != null ? todo.managers.any().user.nickname.contains(nickname) : null ;
}
// 생성일 범위 검색 메서드
private BooleanExpression createdDateBetween(LocalDate start, LocalDate end) {
return todo.createdAt.between(start.atStartOfDay(), end.atTime(LocalTime.MAX));
}
// start 날짜 이상 범위 검색 메서드
private BooleanExpression createdDateAfter(LocalDate start) {
return todo.createdAt.goe(start.atStartOfDay());
}
// end 날짜 이하 범위 검색 메서드
private BooleanExpression createdDateBefore(LocalDate end) {
return todo.createdAt.loe(end.atTime(LocalTime.MAX));
}
todo.managers.size(), // 담당자 수
todo.comments.size() // 댓글 수
todo.managers.size(), todo.comments.size() 를 이용해서 몇 개의 데이터가 있는지 확인할 수 있음.
.leftJoin(todo.managers)
.leftJoin(todo.comments)
leftJoin 을 사용한 이유는 todo 는 있는데 comments, managers 가 없는 경우에 null 값으로라도 들어가서 조회시켜야하기 때문에 leftJoin 을 사용함.
.orderBy(todo.createdAt.desc()) // 생성일 최신순 조회
createdAt 순으로 내림차순 정렬한다.
.offset(pageable.getOffset()) // 페이지 넘버
.limit(pageable.getPageSize()) // 페이지 크기 ( 페이지당 할일 표시 수 )
페이지네이션을 위한 offset, limit 설정
long total = Optional.ofNullable(queryFactory
.select(todo.count())
.from(todo)
.where(builder)
.fetchOne())
.orElse(0L);
페이지네이션을 위해 total 값을 얻기 위해서 todo.count() 호출
// 닉네임 검색 포함 조회 메서드
private BooleanExpression nicknameContains(String nickname) {
return nickname != null ? todo.managers.any().user.nickname.contains(nickname) : null ;
}
nickname 을 받고 QueriyDSL 에서 조건을 표현하는데에 사용하는 객체인 BooleanExpression 으로 반환.
먼저 입력된 nickname 이 null 인지 확인. null 이라면 조건을 생성하고, null 이라면 null 그대로 반환
조건
todo.managers.any().user.nickname.contains(nickname)
- todo.managers.any(): Todo 엔티티에서 managers 컬렉션의 어떤 항목이라도 선택하는 부분임.
- 이때 any()는 managers 리스트에 하나 이상의 Manager 객체가 있을 때 조건을 적용함.
- user.nickname.contains(nickname): 선택된 Manager의 User 엔티티에서 nickname 속성을 참조함. contains(nickname)는 nickname이 User의 nickname에 포함되어 있는지를 체크
'컴퓨터 프로그래밍 > Spring' 카테고리의 다른 글
[Spring] Discord 알림 구현 (0) | 2024.10.16 |
---|---|
[Spring] TransactionalEventListener (0) | 2024.10.11 |
[Spring] Spring Security (0) | 2024.10.04 |
[Spring] QueryDSL (1) | 2024.10.03 |
[Spring] Lazy Loading 과 Eager Loading 의 차이, N+1 Problem 과 해결 방법 (1) | 2024.10.03 |