QueryDSL | 활용하기
by jeongminy1. build.gradle.kts에 의존성 추가
여기서 추가하는 kapt는 Kotlin Annotation Processing Tool 입니다.
Annotation들을 분석하여 QueryDSL에 알려줘, QClass 들을 사용할 수 있게 만드는 역할을 합니다.
plugins {
kotlin("kapt") version "1.8.22" // 추가!
}
val queryDslVersion = "5.0.0" // 추가! (queryDsl 버전을 설정)
dependencies{
implementation("com.querydsl:querydsl-jpa:$queryDslVersion:jakarta") // 추가!
kapt("com.querydsl:querydsl-apt:$queryDslVersion:jakarta") // 추가!
}
2. Qclass 생성
기존에 만들어 져 있는 Entity를 기반으로 Qclass가 생성된 걸 볼 수 있다.
단, 변수 자체가 아닌 Path 가 추가되어 경로로 설정되어 있는데 Path를 기반으로 최종적으로 SQL을 만들어 낸다는 점을 알 수 있다.
3. QueryFactory 생성
QueryDsl도 마찬가지로 DB를 조회하므로, Repository의 역할을 한다고 볼 수 있다.
모든 Repository에서 사용할 테니 다른데서도 공통으로 사용할 수 있는 QueryFactory 를 만들어 준다.
//이 클래스를 상속받은 구체적인 클래스에서는
//queryFactory를 사용하여 QueryDSL을 활용하여 SQL 쿼리를 작성하고 실행할 수 있습니다.
abstract class QueryDslSupport { //해당 클래스를 직접 사용할게 아니므로 abstract 추상클래스로 만들어줌
@PersistenceContext
//해당 어노테이션을 사용하여 EntityManager를 주입받습니다.
//EntityManager는 JPA(Java Persistence API)를 사용하여
//데이터베이스와의 상호 작용을 담당하는 중요한 컴포넌트입니다.
protected lateinit var entityManager: EntityManager //protected를 줌으로써, 상속받는 애들이 사용할 수 있게 함.
//JPAQueryFactory는 QueryDSL에서 제공하는 쿼리 작성을 위한 핵심 클래스로,
//EntityManager를 기반으로 쿼리를 생성하고 실행할 수 있습니다.
protected val queryFactory: JPAQueryFactory //protected를 줌으로써, 상속받는 애들이 사용할 수 있게 함.
get() {
return JPAQueryFactory(entityManager)
}
}
QueryDslCourseRepository 생성
@Repository //외부기능을 활용하다 보니 Repository 어노테이션을 붙여 주어야 함. 그래야 spring Bean으로서 동작함.
class QueryDslCourseRepository: QueryDslSupport() {
private val course = QCourse.course //course 에 대한 QClass를 멤버 변수로 선언 해준다.
//title 기준으로 course를 조회함
fun searchCourseListByTitle(title: String): List<Course>{
return queryFactory.selectFrom(course)
.where(course.title.containsIgnoreCase(title)) //containsIgnoreCase 는 대소문자 구분 하지 않고 검색해 줘서 좋음.
.fetch()
}
}
4. 제목 기준으로 목록 조회 기능 구현하기
Controller 작성
@RequestMapping("/courses")
@RestController
class CourseController(
private val courseService: CourseService
) {
@GetMapping("/search")
@PreAuthorize("hasRole('TUTOR') or hasRole('STUDENT')")
fun searchCourseList(@RequestParam(value = "title") title: String): ResponseEntity<List<CourseResponse>> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.searchCourseList(title))
}
//이하 생략
}
service 추가
interface CourseService {
//생략
fun searchCourseList(title: String): List<CourseResponse>?
//생략
}
service 인터페이스에 메소드를 추가하고,
serviceImpl 구현부에 가서 queryDslCourseRepository를 생성자 주입하고,
getAllCourseList 를 구현한다.
@Service
class CourseServiceImpl(
private val courseRepository: CourseRepository,
private val queryDslCourseRepository: QueryDslCourseRepository, //주입
private val lectureRepository: LectureRepository,
private val courseApplicationRepository: CourseApplicationRepository,
private val userRepository: UserRepository,
) : CourseService {
override fun getAllCourseList(): List<CourseResponse> {
return courseRepository.findAll().map { it.toResponse() }
}
//생략
}
5. Repository 구조 변경
단! 여기서
private val courseRepository: CourseRepository,
private val queryDslCourseRepository: QueryDslCourseRepository
repository를 두개나 종속성에 추가해주어야해서 내심 찝찝하다...!
둘 다 결국 DB와 연결되어 쿼리를 수행하는 역할인데,
어떤 방식으로 쿼리를 작성하느냐에 따라 저렇게 나뉘어 버린 것이다.
이 부분을 해결하기 위해 아래처럼 구성하면 된다!
따라서, CustomCourseRepository를 만들어 준 후,
CourseRepository에 CustomCourseRepository 를 상속해준다.
Repository의 패키지 구조가 이렇게 될 것이다.
6. Pageable 인터페이스
이 인터페이스는 pagination을 지원하기 위한 메서드와 속성을 제공합니다.
해당 인터페이스를 구현하는 클래스는 페이지네이션 기능을 활용하여 데이터를 조회하고, 정렬하고, 페이지를 관리할 수 있습니다. 이를 통해 효율적인 데이터 검색과 페이지네이션을 구현할 수 있습니다.
public interface Pageable {
//페이지네이션 없이 모든 결과를 반환하는 Pageable 객체를 생성하여 반환합니다.
static Pageable unpaged() {
return Unpaged.INSTANCE;
}
//주어진 페이지 크기로 초기화된 첫 번째 페이지를 나타내는 Pageable 객체를 생성하여 반환합니다.
static Pageable ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}
//현재 Pageable이 페이지네이션된 상태인지 여부를 반환합니다.
default boolean isPaged() {
return true;
}
//현재 Pageable이 페이지네이션되지 않은 상태인지 여부를 반환합니다.
default boolean isUnpaged() {
return !isPaged();
}
//현재 페이지 번호를 반환합니다.
int getPageNumber();
//페이지 크기를 반환합니다.
int getPageSize();
//현재 페이지의 오프셋(시작 인덱스)을 반환합니다.
long getOffset();
//페이지 결과를 정렬하는 데 사용되는 Sort 객체를 반환합니다.
Sort getSort();
//페이지 결과를 정렬하는 데 사용되는 Sort 객체를 반환합니다.
//만약 현재 Sort가 정렬되지 않은 상태라면, 주어진 fallback Sort를 반환합니다.
default Sort getSortOr(Sort sort) {
Assert.notNull(sort, "Fallback Sort must not be null");
return getSort().isSorted() ? getSort() : sort;
}
//다음 페이지를 나타내는 새로운 Pageable 객체를 반환합니다.
Pageable next();
//이전 페이지를 나타내는 새로운 Pageable 객체를 반환합니다.
//이전 페이지가 없을 경우 첫 번째 페이지를 반환합니다.
Pageable previousOrFirst();
//첫 번째 페이지를 나타내는 새로운 Pageable 객체를 반환합니다.
Pageable first();
//주어진 페이지 번호로 초기화된 새로운 Pageable 객체를 반환합니다.
Pageable withPage(int pageNumber);
//이전 페이지가 있는지 여부를 반환합니다.
boolean hasPrevious();
//현재 Pageable 객체를 Optional로 감싸서 반환합니다.
//페이지네이션이 되지 않은 경우 Optional.empty()를 반환합니다.
default Optional<Pageable> toOptional() {
return isUnpaged() ? Optional.empty() : Optional.of(this);
}
//현재 Pageable 객체를 OffsetScrollPosition으로 변환하여 반환합니다.
//페이지네이션이 되지 않은 경우 예외가 발생합니다.
default OffsetScrollPosition toScrollPosition() {
if (isUnpaged()) {
throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
}
return ScrollPosition.offset(getOffset());
}
}
이제 controller에서 pageable을 추가해주기만 해도, 이 안에서 알아서 page, size sort 가 자동으로 붙는다.
swagger를 확인해 보면 눈으로 확인 가능ㅎ
default값을 내가 원하는 대로 설정할 수 도 있다.
여기서, 반환할 때 현재 List 인 것을 볼 수 있다.
하지만 예를 들어, front에서 page를 표기할 때,
마지막 페이지가 몇인지에 대한 정보 등을 전해 주어야 front단 에서 페이지를 구성할 수 있을 것이다.
그러므로 이러한 정보들을 같이 주는게 좋다.
따라서 List로 반환 하는 것이 아니라 Page로 반환 하는 것이 좋아!
7. Page 인터페이스
Page<T> 인터페이스는 페이징된 데이터를 표현하고, 페이지 정보와 요소에 대한 다양한 작업을 수행할 수 있는 메서드들을 제공합니다. 구체적인 구현은 PageImpl<T> 클래스에서 이루어지며, 이 인터페이스를 구현하는 다른 클래스에서는 해당 메서드들을 구현해야 합니다.
public interface Page<T> extends Slice<T> {
//빈 페이지(Page<T>)를 반환하는 정적 메서드입니다.
//Pageable.unpaged()를 사용하여 빈 페이지를 생성합니다.
static <T> Page<T> empty() {
return empty(Pageable.unpaged());
}
//주어진 pageable을 사용하여 빈 페이지(Page<T>)를 반환하는 정적 메서드입니다.
//Collections.emptyList()를 사용하여 빈 요소 리스트를 생성하고,
//이를 pageable과 함께 PageImpl<T> 객체로 감싸서 반환합니다.
static <T> Page<T> empty(Pageable pageable) {
return new PageImpl<>(Collections.emptyList(), pageable, 0);
}
//총 페이지 수를 반환하는 메서드입니다.
int getTotalPages();
//총 요소 수를 반환하는 메서드입니다.
long getTotalElements();
//요소 변환기(converter)를 사용하여
//현재 페이지의 요소를 다른 타입(U)으로 변환한 새로운 페이지(Page<U>)를 반환하는 메서드입니다.
<U> Page<U> map(Function<? super T, ? extends U> converter);
}
8. CourseStatus 조건에 따라 조회 기능 구현하기.
CourseController
@RequestMapping("/courses")
@RestController
class CourseController(
private val courseService: CourseService
) {
@GetMapping
@PreAuthorize("hasRole('TUTOR') or hasRole('STUDENT')")
fun getCourseList(
@PageableDefault(
size=15,
sort = ["id"]
) pageable:Pageable,
@RequestParam(value = "status", required = false) status: String?
): ResponseEntity<Page<CourseResponse>> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getPaginatedCourseList(pageable, status))
}
//이하 생략
}
CourseServiceImpl
@Service
class CourseServiceImpl(
private val courseRepository: CourseRepository,
private val lectureRepository: LectureRepository,
private val courseApplicationRepository: CourseApplicationRepository,
private val userRepository: UserRepository,
) : CourseService {
override fun getPaginatedCourseList(pageable: Pageable, status: String?): Page<CourseResponse>? {
val courseStatus = when (status){
"OPEN" -> CourseStatus.OPEN
"CLOSED" -> CourseStatus.CLOSED
null -> null
else -> throw IllegalArgumentException("The status is invalied")
}
return courseRepository.findByPageableAndStatus(pageable, courseStatus).map {it.toResponse()}
}
CourseRepositoryImpl
override fun findByPageableAndStatus(pageable: Pageable, courseStatus: CourseStatus?): Page<Course> {
val whereClause = BooleanBuilder()
courseStatus?.let {whereClause.and(course.status.eq(courseStatus))}
val totalCount = queryFactory.select(course.count()).from(course).where(whereClause).fetchOne() ?: 0L
val query = queryFactory.selectFrom(course)
.where(whereClause)
.offset(pageable.offset)
.limit(pageable.pageSize.toLong())
if (pageable.sort.isSorted) { //정렬 조건이 있는지 학인하고
when(pageable.sort.first()?.property) { //만약에 정렬 조건이 하나만 있을때, 그 조건이
"id" -> query.orderBy(course.id.asc()) //id라면
"title" -> query.orderBy(course.title.asc()) //title이라면
else -> query.orderBy(course.id.asc()) //이 외에는 courseId로 정렬한다.
}
} else { //만약에 정렬 조건이 없다면
query.orderBy(course.id.asc()) //courseId로 정렬한다.
}
val contents = query.fetch()
return PageImpl(contents, pageable, totalCount)
}
'📖 Study > JPA, QueryDSL' 카테고리의 다른 글
JPA, QueryDSL | 심화 (0) | 2024.02.08 |
---|
블로그의 정보
꿈틀꿈틀 개발일기
jeongminy