Develop/spring-data

[kotlin] Querydsl-JPA GroupBy 사용했을 경우 Paging처리 방법

에디개발자 2021. 8. 6. 07:00
반응형

나를 닮았다고 한다...

들어가기 전에

Querydsl-JPA를 사용할 때 Query에 GroupBy 절이 포함된다면 fetchCount(), fetchResults() 메서드를 사용할 수 없습니다. 정확히 count() 를 사용할 수 없습니다. 이럴 경우 PageImpl을 사용하여 Paging 처리해야 하는 경우에 대해서 정리해보겠습니다. 

 

모든 소스는 Github에 올려두었습니다.

 

에러발생

GroupBy절을 포함하고 fetchCount(), fetchResults() 메서드를 사용하면 아래와 같은 에러가 발생합니다.

Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: having near line 5, column 1

 

GroupBy를 사용하는 경우 분기처리

김영한님 인프런 답변을 참조하여 클래스를 작성하였습니다. 복잡한 Query를 사용할 경우 fetch() 메서드를 사용하여 total count를 조회해야합니다. 

 

데이터의 양이 많아진다면 성능상에 문제가 생기므로 fetch()를 사용하여 total count를 조회하기 보단 count() 만을 조회하는 Query를 따로 짜야합니다. 

 

QuerydslPageAndSortRepository

open class QuerydslPageAndSortRepository(
    private val entityManager: EntityManager,
    private val clazz: Class<*>
) {

    private fun getQuerydsl(): Querydsl {
        val builder = PathBuilderFactory().create(clazz)
        return Querydsl(entityManager, builder)
    }

    /**
     * Paging 처리 결과값 조회
     *   - Query Paging 결과값
     *   - Pageable 객체
     *   - Query total Count
     * @param pageable Pageable
     * @param query JPQLQuery<T>
     * @return PageImpl<T>
     */
    fun <T> getPageImpl(pageable: Pageable, query: JPQLQuery<T>): PageImpl<T> {
        return if (query.metadata.groupBy.size > 0) {
            getPageImplIfGroupBy(pageable, query)
        } else {
            getPageImplIfNotGroupBy(pageable, query)
        }
    }

    /**
     * GroupBy절을 사용하는 Query
     * @param pageable Pageable
     * @param query JPQLQuery<T>
     * @return PageImpl<T>
     */
    private fun <T> getPageImplIfGroupBy(pageable: Pageable, query: JPQLQuery<T>): PageImpl<T> {
        val queryResult = query.fetch()
        val totalCount = queryResult.size

        val offset = pageable.offset

        // totalCount 보다 큰 값이 들어온 경우
        if (offset > totalCount) {
            return PageImpl(listOf(), pageable, totalCount.toLong())
        }

        // limit 설정
        var limit = pageable.pageSize * (pageable.pageNumber + 1)
        limit = if (limit > totalCount) {
            totalCount
        } else {
            limit
        }

        val results = queryResult.subList(offset.toInt(), limit)
        return PageImpl(results, pageable, totalCount.toLong())
    }

    /**
     * GroupBy절을 사용안하는 Query
     * @param pageable Pageable
     * @param query JPQLQuery<T>
     * @return PageImpl<T>
     */
    private fun <T> getPageImplIfNotGroupBy(pageable: Pageable, query: JPQLQuery<T>): PageImpl<T> {
        val totalCount = query.fetchCount()

        val results = getQuerydsl()
            .applyPagination(pageable, query)
            .fetch()

        return PageImpl(results, pageable, totalCount)
    }
}

1. query meta 정보에서 groupBy 개수를 체크

2. groupBy 가 있을 경우 query.fetch()하여 전체 리스트 조회

3. 리스트의 개수로 total count 조회

4. kotlin의 subList()를 사용하여 query의 offset, limit과 동일한 결과를 추출

  - 주의할 점 : subList는 전체 리스트의 개수보다 많은 숫자를 설정할 경우 에러발생하므로 예외 처리 필수!

 

사용방법

open class StaffCustomRepositoryImpl(
    private val queryFactory: JPAQueryFactory,
    private val entityManager: EntityManager
) : StaffCustomRepository, QuerydslPageAndSortRepository(entityManager, Staff::class.java) {
// 1)

    /**
     * group by 절 Paging 처리
     * @param pageable Pageable
     * @return PageImpl<StaffGroupByVO>
     */
    override fun selectGroupById(pageable: Pageable): PageImpl<StaffGroupByVO> {
        val queryResult = queryFactory
            .select(
                Projections.fields(
                    StaffGroupByVO::class.java,
                    staff.name,
                    house.id.count()
                )
            )
            .from(staff)
            .join(staff.house, house)
            .groupBy(house.id)
        
        return super.getPageImpl(pageable, queryResult)  // 2)
    }
}

 

1) 위에서 구현한 QuerydslPageAndSortRepository 클래스를 상속받습니다.

2) getPageImpl() 메서드를 호출하여 사용합니다.

 

주의사항

이 방법은 성능상 문제로 데이터의 양이 많지 않을 경우에만 사용하는 것을 권해드립니다.

데이터 양이 많아진다면 total count만을 조회하는 query를 생성하거나 total count를 조회하지 않도록 서비스를 푸는 방향으로 고려해주실 바랍니다.

반응형