티스토리 뷰

● 개요

QueryDsl의 전체 카운팅을 위해 fetchCount를 잘 사용하다가 해당 함수가 deprecated 되어 전체 카운트를 구하는 방법에 대해 고민이 많아졌다.  해당 이슈에 대해 많은 분들이 고민하지 않을까 생각이 든다. 누군가 명확한 해결책을 제시해 주길 바랬지만 아직 찾지 못했습니다.

그동안 사용한 QueryDsl을 걷어내고 nativeSql을 사용해야 하는것일까?

아니면 counting만 nativeSql을 사용해야 하는것일까? 그렇게 되면 동적 쿼리나 파라미터 세팅을 이중으로 관리하게 되어 더 복잡해 보여 hql을 nativeSql로 변경하여 counting 하는 방법일 고민해보았다.


● queryDsl로 counting 구하기

getTotalCount(JPAQuery<?> jpaQuery) 의 라인별 설명입니다.

1. hql -> nativeSql로 변환한다.

2. nativeSql에 count() 함수를 추가한다.

3. EntityManager nativeSql를 추가하여 javax.persistence.Query를 만든다.

4. hql -> nativeSql용으로 파라미터 바인딩을 해준다.

    - 저는 순차 바인딩만 적용하였습니다. 네이밍 바인딩시에는 커스텀이 필요합니다.

    - 바인딩시 제가 발견한 이슈는 두개입니다.

        첫째, nativeSql의 바인딩은 단순히 물음표(?, ?...)만 나오지만 Query를 만들면 물음표에 숫서(?1, ?2...) 형태로
        구조를 잡아주셔야 합니다.
        둘째, enum의 바인딩은 지원하지 않습니다. String으로 변환해줘야 합니다.

5. 쿼리를 실행하여 전체 카운팅을 조회한다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.hql.internal.ast.QueryTranslatorImpl;
import org.springframework.stereotype.Component;

import com.querydsl.jpa.HQLTemplates;
import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAUtil;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class QueryDslSupport {

    @PersistenceContext
    private EntityManager entityManager;

    // native sql 바인딩에서 queryDsl 파라미터 위치 바인딩 형태로 변경 (?, ?… → ?1, ?2…)
    private static String nativeSqlToQueryDslParameterBinding(String sql) {
        final var number = new AtomicInteger(1);
        return sql.chars()
                  .mapToObj(c -> String.valueOf((char) c))
                  .map(c -> "?".equals(c) ? String.format("?%d", number.getAndIncrement()) : c)
                  .collect(Collectors.joining(""));
    }

    // enum은 string으로 변경하여 파라미터 매핑
    private static ArrayList<Object> sqlParameterEnumToString(List<Object> constants) {
        final var nativeSqlParameter = new ArrayList<>();
        for (var constant : constants) {
            if (constant instanceof ArrayList<?> arrayList && arrayList.get(0) instanceof Enum) {
                nativeSqlParameter.add(arrayList.stream().map(Object::toString).toList());
            } else if (constant instanceof Enum) {
                nativeSqlParameter.add(constant.toString());
            } else {
                nativeSqlParameter.add(constant);
            }
        }
        return nativeSqlParameter;
    }

    public long getTotalCount(JPAQuery<?> jpaQuery) {
        final var nativeSql = convertNativeSql(jpaQuery);
        final var stringCountQuery = String.format("SELECT COUNT(1) FROM (%s) as count", nativeSql);
        final var countQuery = entityManager.createNativeQuery(stringCountQuery);
        JPAUtil.setConstants(countQuery, getParameter(jpaQuery), null);
        return Long.parseLong(countQuery.getSingleResult().toString());
    }

    // jpaQuery의 hql을 nativeSql로 변경한다.
    // JPAQuery를 사용하지 않았다면 hql을 추출하는 jpaQuery.toString() 부분을 맞게 수정하면된다.
    private String convertNativeSql(JPAQuery<?> jpaQuery) {
        final var hibernateSession = entityManager.unwrap(SessionImplementor.class);
        final var queryTranslator = new QueryTranslatorImpl("", jpaQuery.toString(), Collections.emptyMap(), hibernateSession.getFactory());
        queryTranslator.compile(Collections.emptyMap(), false);
        return nativeSqlToQueryDslParameterBinding(queryTranslator.getSQLString());
    }

    // JPAQuery에서 파라미터를 추출한다.
    private List<Object> getParameter(JPAQuery<?> jpaQuery) {
        final var serializer = new JPQLSerializer(HQLTemplates.DEFAULT, entityManager);
        serializer.serialize(jpaQuery.getMetadata(), false, null);
        return sqlParameterEnumToString(serializer.getConstants());
    }
}

 결론

제가 활용한 쿼리는 단순 group by이기 때문에 counting 하는데 문제가 없었습니다.

하지만 다양한 형태의 쿼리들을 검증한것은 아니기 때문에 문제가 발생한다면 개선이 필요할 수도 있습니다.

위의 내용을 베이직으로 하여 문제를 개선하여 활용하기 바랍니다.


 참고 url

https://github.com/querydsl/querydsl/issues/2504

댓글
공지사항