최근 대량의 데이터들을 집계하여 조회하는 API 프로젝트를 진행 중에,
Spring Data JPA 와 Querydsl 을 이용하여 쿼리를 작성하고 있었다.
오늘, sum 집계 함수 때문에, java object 로 매핑이 안되는 문제가 있었는데,
그 이유와 해결과정을 설명해보겠다.
기존 쿼리
@Query(
"""
select
new service.dto.PaymentTotalSumDTO(
sum(sub.totalAmount) as totalAmount,
sum(sub.discount) as discount,
sum(sub.usedPoint) as usedPoint
)
from
(select
tpd.totalAmount as totalAmount,
tpd.discount as discount,
tpd.usedPoint as usedPoint,
from TransactionPaymentDetail tpd
where tpd.sid = :sid
group by
tpd.pid,
tpd.totalAmount,
tpd.discount,
tpd.usedPoint) sub
"""
)
fun findTotalSumBySid(
sid: String
): PaymentTotalSumDTO?
data class PaymentTotalSumDTO(
val totalAmount: BigDecimal = BigDecimal.ZERO,
val discount: BigDecimal = BigDecimal.ZERO,
val usedPoint: BigDecimal = BigDecimal.ZERO,
)
JPQL 로 groupby 하여 얻은 서브쿼리에서 각 컬럼별로 sum 을 때려 DTO 로 조회하는 함수이다.
해당 query 는 이러한 에러를 발생시키며, 실패한다.
Caused by: java.lang.IllegalArgumentException: org.hibernate.QueryException: Function argument [SqmBasicValuedSimplePath(<<derived>>(sub).totalAmount)] of type [org.hibernate.query.derived.AnonymousTupleSqmPathSource@59282e4a] at specified position [1] in call arguments was not typed as an allowable function return type
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:141)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:374)
at org.hibernate.query.sq m.internal.QuerySqmImpl.list(QuerySqmImpl.java:1073)
at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:457)
at org.hibernate.query.sqm.internal.QuerySqmImpl.getSingleResult(QuerySqmImpl.java:1103)
무엇이 문제였을까?
- 하이버네이트의 집계함수 sum 은 long 타입을 반환한다.
BigDecimal 타입과 Long 타입이 일치하지 않아, object 로 매핑이 되지 않는 것이다.
어떻게 해야 할까?
@Query(
"""
select
new service.dto.PaymentTotalSumDTO(
sum(cast(sub.totalAmount as bigdecimal)) as ta,
sum(cast(sub.discount as bigdecimal)) as discount,
sum(cast(sub.usedPoint as bigdecimal)) as usedPoint
)
from
(select
tpd.totalAmount as totalAmount,
tpd.discount as discount,
tpd.usedPoint as usedPoint,
from TransactionPaymentDetail tpd
where tpd.sid = :sid
group by
tpd.pid,
tpd.totalAmount,
tpd.discount,
tpd.usedPoint) sub
"""
)
fun findTotalSumBySid(
sid: String
): PaymentTotalSumDTO?
- cast 함수를 이용하여 bigdecimal 타입으로 캐스팅 해주었다.
서브 쿼리인 sub 는 실행시점에 entity 에 명시된 타입대로 bigdecimal 타입을 반환하지만,
sub 를 이용하여 select 하는 구간에서 하이버네이트는 type 적용이 되지 않아버린다.
DB 단에서 한번에 쿼리하는 것이 아닌, 애플리케이션 메모리에서 long 을 반환하는 sum 함수가 실행되기 때문이다.
그럼 정상적으로 쿼리가 실행되는 것을 알 수 있다.
According to the JPA spec 4.5.8
The Java type that is contained in the result of a query using an aggregate function is as follows:
• COUNT returns Long.
• MAX, MIN return the type of the state field to which they are applied.
• AVG returns Double.
• SUM returns Long when applied to state fields of integral types (other than BigInteger); Double when applied to state fields of floating point types; BigInteger when applied to state fields of type BigInteger; and BigDecimal when applied to state fields of type BigDecimal.
참조 :
https://stackoverflow.com/questions/61954687/jpa-sum-returns-long
'📗 JPA' 카테고리의 다른 글
MapStruct! JPA Entity 매핑 간 주의해서 사용하자 (0) | 2024.03.24 |
---|---|
[Spring Data JPA] JPA Enum 필드에 관한 문제 (1) | 2023.02.21 |
[Spring Data JPA] 한방 쿼리의 효능 (feat : JPQL) (0) | 2023.02.01 |
[Spring Data JPA] Transaction 없이 읽기 (0) | 2023.01.30 |
JPA 개념 정리 (0) | 2022.07.15 |