Kotlin + Springboot + JPA + QueryDsl 환경에서 커스텀 PageRequest로 자동 정렬 설정하기
페이지네이션을 설정 하면서 QueryDsl orderBy에 order할 설정을 하나하나 해줘야 하는 상황... 대강 아래와 같은 코드들을 정렬 설정이 필요하다면 계속 추가해줘야 했다.
컬럼이 많을 수록 정렬값이 더 필요하게 됐을때 의미없는 설정을 계속 해줘야하는 번거로움? 같은게 있다고 느껴졌고 동적으로 설정할 수 있을것 같은 느낌이 들어 설정을 찾게 되었다.
.orderBy(
employeeClothes.id.desc(),
employeeClothes.nickname.asc(),
)
import org.hibernate.annotations.Comment
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "tb_temp")
class Temp(
@Column(name = "name", length = 50, nullable = false)
@Comment("이름")
var name: String,
@Column(name = "memo", length = 255)
@Comment("메모")
var memo: String? = null,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "temp_id")
val id: Long? = null,
) {
// 동적 정렬에서 사용할 class Type 쓰기 위해서 추가하였음.
companion object Temp
}
package 패키지.경로
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
class PageRequest {
private var page: Int = 1
private var size: Int = 10
private var direction: Sort.Direction = Sort.Direction.DESC
// 페이지네이션 사용 여부 / false로 주면 모든 데이터를 가져옴
private var paged = true
fun setPage(page: Int) {
this.page = if (page <= 0) 1 else page
}
fun setSize(size: Int) {
val DEFAULT_SIZE = 10
val MAX_SIZE = Int.MAX_VALUE
this.size = if (size > MAX_SIZE) DEFAULT_SIZE else size
}
fun setDirection(direction: Sort.Direction) {
this.direction = direction
}
fun setPaged(paged: Boolean) {
this.paged = paged
}
fun of(): Pageable {
return if(paged) {
PageRequest.of(page - 1, size, direction, "id")
} else {
Pageable.unpaged()
}
}
}
먼저 DTO를 동적으로 값을 받아올 수 있도록 수정해야한다.
형식은 sort={name}.{direction},{name2}.{direction}... 의 형식으로 구성하였다.
필요하거나 무조건 변경을 해야 한다면 fun of() 내용을 수정하면 된다.
class PageRequest {
private var page: Int = 1
private var size: Int = 10
// private var direction: Sort.Direction = Sort.Direction.DESC
// custom sort information
private var sort: List<String>? = null
// 페이지네이션 사용 여부 / false로 주면 모든 데이터를 가져옴
private var paged = true
fun setPage(page: Int) {
this.page = if (page <= 0) 1 else page
}
fun setSize(size: Int) {
val DEFAULT_SIZE = 10
val MAX_SIZE = Int.MAX_VALUE
this.size = if (size > MAX_SIZE) DEFAULT_SIZE else size
}
// fun setDirection(direction: Sort.Direction) {
// this.direction = direction
// }
fun setSort(sort: List<String>?) {
this.sort = sort
}
fun setPaged(paged: Boolean) {
this.paged = paged
}
fun of(): Pageable {
// 사용자 정의 sort가 오지 않았을때 id desc를 기본으로 사용하기 위해 생성.
val defaultOrder = Sort.Order.desc("id")
val orders: List<Sort.Order> = sort?.filter {
// name.asc 같은 형태로 와야 정렬처리 하므로 필터링한다.
// 외래 테이블 조인해야하는 경우 table.column.asc 형태로 오기 때문에 2이상으로 필터링한다.
it.split(".").size >= 2
}?.map {
// .으로 필터링된 정렬 값을 분리시킨다.
val split = it.split(".")
// properties의 정렬 방향을 설정한다.
// split의 0부터 n-1까지
val lastValueIndex = split.size - 1
val properties = split.subList(0, lastValueIndex).joinToString(".")
// split의 마지막 값은 항상 split.size - 1임.
val directionValue = split[lastValueIndex].equals("asc", true)
// direction은 asc로 왔을때만 asc로 설정한다.
val direction = if (directionValue) Sort.Direction.ASC else Sort.Direction.DESC
Sort.Order(direction, properties)
} ?: listOf(defaultOrder)
// 위에서 계산한 값으로 정렬값 생성
val sortBy = Sort.by(orders)
return if (paged) {
PageRequest.of(page - 1, size, sortBy)
} else {
Pageable.unpaged()
}
}
}
interface PaginationRepository {
fun getOffset(pageable: Pageable?): Long {
return if (pageable != null && pageable.isPaged) {
pageable.offset
} else {
0
}
}
fun getLimit(pageable: Pageable?): Long {
return if (pageable != null && pageable.isPaged) {
pageable.pageSize.toLong()
} else {
Long.MAX_VALUE
}
}
}
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.PathBuilder;
import org.springframework.data.domain.Sort;
import java.util.ArrayList;
import java.util.List;
interface PaginationSortRepository : PaginationRepository {
fun getOrderSpecifiers(sort: Sort, classType: Class<*>, className: String?): Array<OrderSpecifier<*>> {
val orders: MutableList<OrderSpecifier<*>> = ArrayList()
// Sort
sort.stream().forEach { order: Sort.Order ->
val direction = if (order.isAscending) Order.ASC else Order.DESC
val orderByExpression: PathBuilder<*> = PathBuilder(classType, className)
orders.add(OrderSpecifier(direction, orderByExpression[order.property] as Expression<Comparable<*>>))
}
return orders.toTypedArray()
}
}
// controller
@RestController
@RequestMapping("/api/v1/temp")
class TempController(
private val tempService: TempService,
) {
@GetMapping("/detail/list")
@ResponseStatus(HttpStatus.OK)
fun getLaundries(
searchCondition: SearchCondition,
pageRequest: PageRequest,
): Page<EmployeeClothesDetailResponse> {
val pageable = pageRequest.of()
return tempService.getDetailList(searchCondition, pageable)
}
}
// service
@Service
class TempService(
private val tempQueryRepository: TempQueryRepository,
) {
@Transactional(readOnly = true)
fun getDetailList(
searchCondition: SearchCondition,
pageable: Pageable,
): Page<TempResponse> {
return tempQueryRepository.findDetailAll(searchCondition, pageable)
}
}
@Repository
// JPAQueryFactory를 사용하려면 QueryDslConfig 파일에 Bean 등록 해줘야함.
class TempQueryRepository(
private val queryFactory: JPAQueryFactory,
) : PaginationSortRepository {
fun findDetailAll(
searchCondition: searchCondition,
pageable: Pageable,
): Page<TempResponse> {
// 테스트 조회
val list = queryFactory.select(
QTemp(
temp.id,
temp.name,
temp.memo,
)
).from(employeeClothes)
.where(
search(searchCondition)
)
.orderBy(*getOrderSpecifiers(pageable.sort, Temp::class.java, "temp"))
// .orderBy(employeeClothes.id.desc())
.offset(getOffset(pageable))
.limit(getLimit(pageable))
.fetch()
// 데이터 총 개수
val count = queryFactory.select(temp.id.count())
.from(temp)
.where(
search(searchCondition)
)
.fetchOne() ?: 0
return PageImpl(list, pageable, count)
}
}
참고자료