-
QueryDSL에서 SubQuery 에는 limit 를 사용 할 수 없다.JPA 2023. 11. 15. 16:56test
SubQuery 종류 중
from 절에 사용하는 인라인뷰는 지원하지 않으니 당연히 limit를 사용할 수 없고
select의 Scalar SubQuery 나 where 절 SubQuery에 limit 사용이 안된다는 것을 알게 되었다.
Scalar SubQuery에서 limit 분실
select( new QModel( orders.orderId, JPAExpressions.selectDistinct(orderHistory.message) .from(orderHistory) .where(orderHistory.orderId.eq(orders.orderId)) .orderBy(orderHistory.createDateTime.asc()) .limit(1) ) ) .from(orders) .limit(20) .fetch();
위처럼
QueryDSL에서 select 절에 sub query를 넣는 스칼라 서브쿼리 사용 시
서브쿼리 안에 limit 절이 사라지는 문제가 발생했다.
select tb_order.order_id, (select distinct tb_order_history.message from tb_order_history where tb_order_history.order_id=tb_order.order_id order by tb_order_history.create_datetime asc) from tb_order limit 20
스칼라 서브 쿼리 안에 limit 절이 사라졌으며
스칼라 서브쿼리는 한 번에 한 가지만을 처리하는 특징으로 인해 결과가 하나의 행이어야 하는데
limit 절이 사라져 여러 행이 나올 수 있게 되어 쿼리 에러가 발생할 수 있다.
이문제에 대한 이슈가 있다.
https://github.com/querydsl/querydsl/issues/3224
QueryDSL 쿼리 변환 과정
밖에 있는 Limit는 정상적인 동작을 하고
스칼라 서브쿼리의 limit 가 사라지는 이유는
QueryDSL로 사용한 쿼리 오브젝트가 어떻게 쿼리로 변환되어 데이터베이스로 요청을 하는지 보면 알 수 있다.
QueryDSL 은
함수를 사용하여 쿼리를 작성할 때 파라미터로 넘긴 값들을 Metadata안에 넣어두고
네이티브 쿼리로 변환 할때 Metadata에서 꺼내어 변환한다.
그중 Limit와 offset는 QueryModifiers 객체로 관리되고 있다
@Override public void setLimit(Long limit) { if (modifiers == null || modifiers.getOffset() == null) { modifiers = QueryModifiers.limit(limit); } else { modifiers = new QueryModifiers(limit, modifiers.getOffset()); } }
Metadata에서 필요한 값을 꺼내서 쿼리를 변환 하는 과정은 createQuery메서드에서 확인이 가능하며
select, from, where, group by, having, order by 절을 완성시키는 과정을 처리하는 serialize 메서드를 호출하며 동작한다.
💡 중요한 건 이 과정에서 select 절이 완성된다는 것이다.
JPA 가 처리 가능한 쿼리 변환 과정은 createQuery에서 처리하게 되는데
Limit와 Offset 은 처리 하지 않고 Query객체에서 따로 담아 실행 프로세스로 이동한다.
이렇게 만들어진 Query객체는 실행 프로세스에 진입하게 되며
실행 직전에 Limit 작업에 들어간다.
private List doQuery( final SharedSessionContractImplementor session, final QueryParameters queryParameters, final boolean returnProxies, final ResultTransformer forcedResultTransformer) throws SQLException, HibernateException { final RowSelection selection = queryParameters.getRowSelection(); final int maxRows = LimitHelper.hasMaxRows( selection ) ? selection.getMaxRows() : Integer.MAX_VALUE; final List<AfterLoadAction> afterLoadActions = new ArrayList<AfterLoadAction>(); final SqlStatementWrapper wrapper = executeQueryStatement( queryParameters, false, afterLoadActions, session ); final ResultSet rs = wrapper.getResultSet(); final Statement st = wrapper.getStatement();
Limit로 넣어두었던 값을 꺼내서
각 데이터베이스의 Dialect 객체에게 limit 작업을 위임하게 되는데
public class MySQLDialect extends Dialect { private static final LimitHandler LIMIT_HANDLER = new AbstractLimitHandler() { @Override public String processSql(String sql, RowSelection selection) { final boolean hasOffset = LimitHelper.hasFirstRow( selection ); return sql + (hasOffset ? " limit ?, ?" : " limit ?"); } }; }
MySQL Dialect는 지금까지 만들어진 SQL Query에 "limit" 문자열을 더하게 된다.
이걸 보면 알 수 있는 것은
QueryDSL로 쿼리를 작성하게 되면
공통쿼리 변환(select, from, where, group by, having, order by) -> 방언 변환(limit, offset) -> 실행의 순서로 진행하게 된다.
스칼라 서브쿼리는 select 구문 이기 때문에 공통 쿼리 변환 작업에서 끝이 나게 되고
공통 쿼리 변환 작업은 limit 절을 작업하지 않기 때문에 limit 이 없는 상태로 작업이 끝나게 되는 것이다.
하지만 밖에 붙은 limit 절은 공통 쿼리 변환 과정에서 생략이 되고 방언 변환에서 추가가 되기 때문에 정상적인 처리가 가능했다.
Where 절 SubQuery에서 limit
위에서 본 것처럼 QueryDSL의 변환 과정을 보면 이 문제는 Scalar SubQuery에서만 발생하는 것이 아닌
Where 절 SubQuery에서도 발생하는 문제라는 것을 알 수 있다.
select(new QTestModel( order.orderId, orderHistory.message)) .from(order) .innerJoin(orderHistory).on(order.orderId.eq(orderHistory.orderId)) .where( orderHistory.createDateTime.eq( JPAExpressions.selectDistinct(orderHistory.createDateTime) .from(orderHistory) .orderBy(orderHistory.createDateTime.desc()) .limit(1) ) ) .limit(20) .fetch();
select tb_order.order_id, tb_order_history.message from tb_order inner join tb_order_history on (tb_order.order_id=tb_order_history.order_id) where orderhisto1_.create_datetime=( select distinct tb_order_history.create_datetime from tb_order_history tb_order_history order by tb_order_history.create_datetime desc ) limit 20
그럼 어떻게 할까?
limit 가 사용이 불가능하면 쿼리를 변경하여 같은 결과물이 나오게 변경하면 된다.
query.select( new QModel( orders.orderId, JPAExpressions.selectDistinct(orderHistory.message) .from(orderHistory) .where( orderHistory.orderId.eq(orders.orderId), orderHistory.createDateTime.eq( JPAExpressions.select(orderHistory.createDateTime.min()) .from(orderHistory) .where(orderHistory.orderId.eq(orders.orderId) ) ) ) ) .from(orders) .limit(20) .fetch();
스칼라 서브쿼리를 조인을 통해 처리하는 방법도 있을 것이고
query.select( new QModel(orders.orderId, orderHistory.message) ) .from(orders) .leftJoin(orderHistory).on(orderHistory.orderId.eq(orders.orderId)) .where( orderHistory.createDateTime.eq( JPAExpressions.select(orderHistory.createDateTime.min()) .from(orderHistory) .where(orderHistory.orderId.eq(orders.orderId) ) ) .limit(20) .fetch();
조인하고 where로 필터 하는 방법을 사용해도 된다.
'JPA' 카테고리의 다른 글
JPA Entity ID 타입 선택시 후보들 (0) 2024.05.27 @Transactional(readOnly = true) 는 Flush 가 동작안할꺼라 믿었다 (0) 2024.04.23 QueryDSL-JPA에서 인라인뷰를 써야 한다면 도망치자 (0) 2023.10.18 JPA Column의 null 제약 조건 (0) 2023.02.07 JPA 에서 GeneratedValue 를 하지않는다는것은 (0) 2022.09.03