[Kotlin + Springboot]QueryDsl 커스텀 페이지네이션 정렬 설정

BOKS·2022년 12월 27일
0

spring

목록 보기
1/1
post-thumbnail

Kotlin + Springboot + JPA + QueryDsl 환경에서 커스텀 PageRequest로 자동 정렬 설정하기

0. QueryDsl에서 동적 정렬을 할 수 있을까?

페이지네이션을 설정 하면서 QueryDsl orderBy에 order할 설정을 하나하나 해줘야 하는 상황... 대강 아래와 같은 코드들을 정렬 설정이 필요하다면 계속 추가해줘야 했다.
컬럼이 많을 수록 정렬값이 더 필요하게 됐을때 의미없는 설정을 계속 해줘야하는 번거로움? 같은게 있다고 느껴졌고 동적으로 설정할 수 있을것 같은 느낌이 들어 설정을 찾게 되었다.

			.orderBy(
                employeeClothes.id.desc(),
                employeeClothes.nickname.asc(),
            )

1. 예시 용 Entity

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
}

2. 페이지네이션에 사용할 커스텀 페이지네이션 DTO

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() 내용을 수정하면 된다.

-- example) sort=name.desc,height.asc...

3. 완성된 커스텀 페이지네이션 DTO

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()
        }
    }
}

4. 페이지네이션 QueryRepository에서 사용할 기본 함수 정의

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()
    }
}

5. 사용 예시

// 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)
    }
}

참고자료

  1. https://uchupura.tistory.com/7
  2. https://itmoon.tistory.com/73
  3. https://velog.io/@seungho1216/Querydsl%EB%8F%99%EC%A0%81-sorting%EC%9D%84-%EC%9C%84%ED%95%9C-OrderSpecifier-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%ED%98%84
profile
Kotlin, Springboot 2 백엔드 개발자

0개의 댓글