회사에서 SQL 쿼리를 작성하다 보니 CASE WHEN을 사용해야 하는 경우가 있었는데(안티패턴으로 취급되기도 하지만) QueryDSL에서 CASE WHEN 과 함께 enum 으로 받고 싶어서 사용해보다가 뭔가 이상한 구석이 있어서 한번 기록으로 적어보았다.
QueryDSL에서 CASE WHEN 사용법의 경우 이곳에 잘 정리되어 있다.
사용법
우선 EnumPath 를 사용할 경우 예시로 아래와 같이 사용할 수 있다.
@RequiredArgsConstructor
public class PartnerSubAccountRepositorySupportImpl implements PartnerSubAccountRepositorySupport {
private final JPAQueryFactory queryFactory;
@Override
public List<SubAccountInfo> test() {
QPartnerSubAccount entity = QPartnerSubAccount.partnerSubAccount;
return queryFactory
.select(
Projections.constructor(SubAccountInfo.class
, entity.seqNo.as("seqNo")
, entity.resellerId
, entity.registDate
, new CaseBuilder()
.when(subAccount.suspendDate.isNotNull())
.then(Expressions.enumPath(SubAccountStatusType.class, "'SUSPEND'"))
.otherwise(Expressions.enumPath(SubAccountStatusType.class, "'USE'"))
.as("statusFlag")
))
.from(entity)
.fetch();
}
}
위의 쿼리를 실행하면 아래처럼 실행된다.
select
*
from
( select
partnersub0_.seqno as col_0_0_,
partnersub0_.reseller_id as col_1_0_,
partnersub0_.registdate as col_2_0_,
case
when partnersub0_.suspenddate is not null then 'SUSPEND'
else 'USE'
end as col_3_0_
from
TESTSCHEMA.PARTNER_SUB_ACCOUNT partnersub0_ )
Expressions.enumPath 사용법은 아래와 같다.
Expressions.enumPath(Enum 클래스, 왼쪽의 Enum 클래스를 valueOf 로 해석할 수 있는 값);
여기서 한 가지 주의해야 할 것이 있다. select 표현식에 숫자 자료형인 데이터를 함께 넣고, 거기에 Path 인터페이스로 할당되지 않도록 만들어주어야 한다.
위의 코드를 보면 entity.seqNo 가 있는데 이는 Entity 에서 Long 으로 정의되어 있고 아래처럼 작성해야 작동하게 된다.
Projections.constructor(SubAccountInfo.class
, entity.seqNo.as("seqNo") // NumberExpression
//, entity.seqNo // NumberPath
enumPath 를 사용하려는데 왜 굳이 이렇게 해야 할까? 이상하다.
참고로 이렇게 하지 않으면 argument type mismatch 라는 오류가 나타난다.
이유
QueryDSL의 경우 JDBC를 통해 데이터를 가져오고 난 후 select 로 가져온 모든 컬럼 값들의 형변환 작업이 이루어지게 되는데 이때 하나의 변환 클래스로 모든 컬럼 값들의 변환이 이루어지고, 이 변환 클래스는 처음 .select() 를 사용할 때 결정된다.
// JPAQueryFactory.java
@Override
public <T> JPAQuery<T> select(Expression<T> expr) {
return query().select(expr);
}
// JPAQuery.java
@Override
public <U> JPAQuery<U> select(Expression<U> expr) {
queryMixin.setProjection(expr);
@SuppressWarnings("unchecked") // This is the new type
JPAQuery<U> newType = (JPAQuery<U>) this;
return newType;
}
// QueryMixin.java
public <E> Expression<E> setProjection(Expression<E> e) {
e = convert(e, Role.SELECT);
metadata.setProjection(e);
return e;
}
// JPAQueryMixin.java
@SuppressWarnings("unchecked")
@Override
public <RT> Expression<RT> convert(Expression<RT> expr, Role role) {
expr = (Expression<RT>) expr.accept(mapAccessVisitor, null);
expr = (Expression<RT>) expr.accept(listAccessVisitor, null);
if (role == Role.ORDER_BY) {
if (expr instanceof Path) {
expr = convertPathForOrder((Path) expr);
} else {
expr = (Expression<RT>) expr.accept(replaceVisitor, null);
}
}
return Conversions.convert(super.convert(expr, role)); // 이 부분에서 결정된다.
}
파고 들어가다보면 사실상 Conversions 라는 클래스에서 반환되는 것을 볼 수 있다. 더 들어가다보면 이런 부분이 있다.
위의 convert 로직을 통해 select 절에 있는 모든 표현식을 살펴보면서 변환에 이용될 클래스를 먼저 찾게 된다.
결국 NumberConversions 또는 인자로 넘어온 ConstantHidingExpression 를 반환하게 되는데, 이 두 개의 클래스는 공통적으로 FactoryExpressionBase 를 상속해서 사용하고 있고, newInstance 라는 메서드를 Override 하고 있다. 이 newInstance 라는 메서드를 기억해두자.
그러면 위의 newInstance 는 어디서 호출될까? QueryDSL에서는 쿼리를 실행하고 아래의 로직을 타게 되면서 변환 작업이 이루어지게 된다.
// Loader.java
private List listIgnoreQueryCache(SharedSessionContractImplementor session, QueryParameters queryParameters) {
return getResultList( doList( session, queryParameters ), queryParameters.getResultTransformer() );
}
// QueryLoader.java
@SuppressWarnings("unchecked")
@Override
protected List getResultList(List results, ResultTransformer resultTransformer) throws QueryException {
// meant to handle dynamic instantiation queries...
HolderInstantiator holderInstantiator = buildHolderInstantiator( resultTransformer );
if ( holderInstantiator.isRequired() ) {
for ( int i = 0; i < results.size(); i++ ) {
Object[] row = (Object[]) results.get( i );
Object result = holderInstantiator.instantiate( row ); // 여기
results.set( i, result );
}
...
// FactoryExpressionTransformer.java
public final class FactoryExpressionTransformer implements ResultTransformer {
private static final long serialVersionUID = -3625957233853100239L;
private final transient FactoryExpression<?> projection;
public FactoryExpressionTransformer(FactoryExpression<?> projection) {
this.projection = projection; // 1
}
...
@Override
public Object transformTuple(Object[] tuple, String[] aliases) {
if (projection.getArgs().size() < tuple.length) {
Object[] shortened = new Object[projection.getArgs().size()];
System.arraycopy(tuple, 0, shortened, 0, shortened.length);
tuple = shortened;
}
return projection.newInstance(tuple); // 2
}
}
아까 전에 말했던 Conversions 에서 반환된 클래스가 위 코드의 1 에 들어가고, 나중에 transformTuple 메서드를 실행하게 되면서 위 코드의 2 를 실행하게 된다. 즉, 위에서 언급했던 newInstance 가 이곳에서 실행된다.
다시 newInstance 메서드를 보자.
NumberConversions 에서는 Enum 변환에 대한 로직이 있는데, ConstantHidingExpression 에서는 Enum 변환에 대한 로직이 없다. Enum 변환을 위해서는 NumberConversions 를 반환하도록 해야 하는데, Conversions 클래스의 needsNumberConversions를 보면 어떻게 해야 하는지 알 수 있다.
// Conversions.java
private static boolean needsNumberConversion(Expression<?> expr) {
expr = ExpressionUtils.extract(expr);
return Number.class.isAssignableFrom(expr.getType()) && !Path.class.isInstance(expr);
}
즉, select 절을 이용할 때 표현식이 숫자 자료형이어야 하고 Path 인터페이스를 사용하지 않아야 한다.
QueryDSL의 경우 Q클래스를 만들고 나면 기본적으로 Entity 내의 컬럼들은 공통적으로 Path 인터페이스를 구현한 클래스를 사용하게 된다.
이를 그대로 사용하면 안 된다는 소리다.
초반에 예시로 QueryDSL 코드를 작성했던 것을 보자.
Projections.constructor(SubAccountInfo.class
, entity.seqNo.as("seqNo") // NumberExpression
//, entity.seqNo // NumberPath
위에서 Q클래스를 통해 컬럼을 사용할 때 위에서 반환되는 것은 얼추 보면 문제 없을 것 같고 비슷한 것 같지만 실제로는 다르다는 것을 알 수 있다.
.as 를 사용하게 되면 Path 인터페이스에서 벗어나므로 NumberConversions 로 이용하게 만들 수 있다.
따라서 enumPath 를 사용할 때 숫자 자료형인 컬럼에 .as 를 사용하여 NumberConversions 로 사용하도록 만들어주어야 비로소 제대로 사용할 수 있다.
'프로그래밍 > Spring' 카테고리의 다른 글
[Spring] OpenFeign @SpringQueryMap 사용 시 별도 파라미터로 사용하기 (0) | 2024.11.15 |
---|---|
[JPA] Querydsl LocalDateTime 에서 Date 로 변환할 때 오류나는 경우 (feat. Oracle) (0) | 2024.03.06 |
[Spring] RestTemplate 과 MessageConverter 순서의 관계 (0) | 2023.02.05 |
[Spring] ConversionService 활용해보기 (0) | 2022.11.21 |
[Spring] AutoConfiguration 직접 만들어서 라이브러리로 만들어보기 (0) | 2022.11.11 |