FROM 절에 서브쿼리를 사용해 보자
들어가며
프로젝트를 진행하면서, 각 카테고리별로 좋아요 수를 기준으로 상위 2개의 게시물을 조회하는 쿼리가 필요하게 되었다. 하지만 일반 SQL문을 사용하는 것이 아닌 QueryDSL을 사용할 때 FROM 절에 서브쿼리를 어떻게 적용할 수 있는지 한번 알아보도록 하겠습니다.
SQL 조회 예시
위와 같이, Article 테이블에서 카테고리별로 좋아요 순으로 상위 2개의 게시물을 조회하는 방법에 대해 설명드리겠습니다.
카테고리별 좋아요 순 쿼리
- 먼저 각 게시물에 ROW_NUMBER()를 사용하여, 카테고리별로 좋아요 순위를 매깁니다. SQL 쿼리는 다음과 같습니다:
SELECT a.*, ROW_NUMBER() OVER (PARTITION BY a.article_category ORDER BY a.like_count DESC) AS rn
FROM article a;
- 이 쿼리를 통해, 각 카테고리별로 게시물의 좋아요 순위가 매겨집니다. 이제 각 카테고리에서 상위 2개의 게시물을 조회해야 합니다.
FROM 절에 카테고리별 좋아요 서브쿼리
- ROW_NUMBER()를 통해 계산된 순위를 이용해, 상위 2개의 게시물만 필터링하기 위해서는 서브쿼리를 FROM 절에 넣어야 합니다. 이를 적용한 SQL은 다음과 같습니다:
SELECT ra.*
FROM(
SELECT a.*, ROW_NUMBER() OVER (PARTITION BY a.article_category ORDER BY a.like_count DESC) AS rn
FROM article a) as ra
where ra.rn<=2;
- 이 쿼리는 각 카테고리별로 좋아요 순으로 정렬된 게시물 중 상위 2개 만을 필터링하여 가져옵니다.
다음과 같이 카테고리별로 좋아요가 많은 게시글을 2개씩 조회된 것을 볼 수 있습니다.
QueryDSL에서 윈도우 함수 사용
기본적으로 JPA와 QueryDSL을 어느 정도 사용해 본 경험이 있다고 간주하고 Q클래스 및 설정 방법 등은 생략하고 작성하도록 하겠습니다.!! ( 스프링부트 3.x 이상버전 사용 중이며, Hibernate 7.0 이상 기준입니다)
먼저 QueryDSL에서의 ROW_OVER() 윈도우 함수 사용방법에 대해서 알아봅시다.
1. Expressions 사용하기
Expressions 사용법
JPAQuery<Tuple> rowNumber = jpaQueryFactory.select(
article,
Expressions.numberTemplate(
Long.class,
"row_number() over (partition by {0} order by {1})",
article.articleCategory,
article.likeCount
).as("rowNumber"))
.from(article);
위와 같이 QueryDsl에서 서브쿼리르 사용할 때 사용하는 Expressions를 사용하여 직접 template를 작성해 주면 됩니다.
쿼리를 실행해 보면 아래와 같이 쿼리가 실행되는 것을 볼 수 있습니다.
2. SQLExpressions사용하기
QueryDSL에서 지원하는 SQLExpressions을 사용하면 됩니다.
rowNumber을 지원하는 것을 볼 수 있습니다.
SQLExpressions을 사용하기 위해서는 다음 의존성을 추가해줘야 합니다.
implementation 'com.querydsl:querydsl-sql:5.0.0'
SQLExpressions 사용법
JPAQuery<Tuple> rowNumber = jpaQueryFactory.select(article, SQLExpressions.rowNumber().over().partitionBy(article.articleCategory).as("rn"))
.from(article)
.orderBy(Expressions.stringPath("rn").desc());
쿼리를 작성하고 실행하게 되면 다음과 같은 오류 메시지를 볼수 있습니다.
발생 원인
- JPQL 쿼리 언어는 SQL 함수인 ROW_NUMBER()와 같은 고급 SQL 표현식을 직접 지원하지 않는다. 따라서, 이를 사용하기 위해서는 JPQL 템플릿을 사용자 정의하여 ORM에 이러한 함수가 사용될 수 있도록 설정해야 하는 오류가 발생합니다.
해결 방법
- jpaQueryFactory를 EntityManger을 통해 빈으로 등록 시에 row_number을 사용하도록 JPQLTemplates의 Enum타입으로 정의된 SQLOps을 등록하여 사용가능하도록 하면 해결할 수 있다.
@Bean
public JPAQueryFactory jpaQueryFactory(){
JPQLTemplates templates = new JPQLTemplates() {
{
add(SQLOps.ROWNUMBER, "row_number()");
}
};
return new JPAQueryFactory(templates,entityManager);
}
위의 주입을 해주게 된다면 정삭적으로 쿼리가 실행되어 조회되는 것을 볼 수 있습니다.
QueryDsl에서 From 절에 서브쿼리 사용하기
From 절에 서브쿼리를 사용하려고 여러 자료를 조사해 본 결과, From 절에서 서브쿼리를 사용하는 것이 가능한지에 대한 정보가 엇갈리고 있었습니다. 어떤 자료에서는 From 절에서 서브쿼리를 사용할 수 없다고 하고, 다른 자료에서는 사용이 가능하다고 설명하고 있었습니다. 여러 시도를 통해 완벽하지는 않지만, 정적인 조회에서는 From 절에 서브쿼리를 사용할 수 있는 방법을 찾게 되었습니다. 이제 이 방법에 대해 자세히 설명드리겠습니다.
From 절에 서브쿼리 사용 시
위에서 row_number을 from절에 쿼리로 사용하게 되면 다음과 같은 에러 메시지와 함께 조회의 실패하는 것을 볼 수 있습니다.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.ClassCastException: class com.querydsl.jpa.impl.JPAQuery cannot be cast to class com.querydsl.core.types.EntityPath (com.querydsl.jpa.impl.JPAQuery and com.querydsl.core.types.EntityPath are in unnamed module of loader 'app')] with root cause
java.lang.ClassCastException: class com.querydsl.jpa.impl.JPAQuery cannot be cast to class com.querydsl.core.types.EntityPath (com.querydsl.jpa.impl.JPAQuery and com.querydsl.core.types.EntityPath are in unnamed module of loader 'app')
발생 원인
- JPQL에서는 서브쿼리를 직접 FROM절에 사용할 수 없기때문에 EntityPath으로 캐스팅을 하더라고 메타데이터인 QAticle로 조회되지 못해 사용할수 없는거 같은 생각이 들었습니다.
해결 방법
- 서브 조회 클래스를 만들어 조회된 Qsub 클래스를 from절에 넣는 방법
- 네이티브 쿼리를 사용하여 조회하는 방법
위에 해결 방법에서는 서브 조회 클래스를 만들어 from절에 사용하는 방법을 알아보도록 하겠습니다.
먼저 본 클래스와 똑같이 클래스를 하나 만들어줍니다.
@Entity
@Subselect(
"SELECT " +
"a.*, " +
"ROW_NUMBER() OVER (PARTITION BY a.article_category ORDER BY a.like_count DESC) AS rn " +
"FROM article a"
)
@Immutable
@Synchronize("article")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ArticleRowSub extends BaseTime {
@Id
@Column(name = "article_id") // assuming this is the primary key
private Long id;
private String title; // 제목
@Column(length = 5000)
private String content; // 내용
private Integer likeCount; // 좋아요 수
}
- @Subselect :
- 서브쿼리의 결과 매핑 : 서브쿼리의 결과를 JPA엔티티로 매핑이 가능하다.
- 쿼리와 엔티티 분리 : 복잡한 쿼리를 엔티티를 통해 매핑하여 복잡하거나 사용할수 없을 때 분리하여 유지보수성 향상
- 복잡한 결과 집합 처리 : @Subselect를 사용하면, 복잡한 집계 함수나 조인 결과를 엔티티로 매핑할 수 있습니다. 이로 인해 복잡한 데이터 집합을 간편하게 처리할 수 있습니다.
- @Immutable :
- 불변(immutable)임을 나타냅니다. 즉, 엔티티의 데이터가 변경되지 않으며, 읽기 전용으로 다루어야 함을 명시합니다.
- @Synchronize :
- 특정 엔티티나 컬렉션에 대한 동시성 문제를 제어하는 데 사용됩니다. article의 데이터의 일관성을 유지합니다.
- 특정 엔티티나 컬렉션에 대한 동시성 문제를 제어하는 데 사용됩니다. article의 데이터의 일관성을 유지합니다.
QueryDsl 쿼리
JPAQuery<ArticleRowSub> from = jpaQueryFactory.select(articleRowSub)
.from(articleRowSub);
article를 사용하여 조회하는 방식이 아닌 ArticleRowSub을 통해 조회하면 from절에 서브쿼리로 사용하는 것과 같은 기능을 동작하게 됩니다.
querydsl 이 만든 쿼리와 직접 실행되는 쿼리가 다른 것을 볼 수 있습니다. from 절에 서브쿼리가 사용되어 조회된것을 볼수 있습니다.
문제점
QueryDSL을 사용하면 동적인 쿼리를 쉽게 생성할 수 있지만, @Subselect를 사용하여 FROM 절에 서브쿼리를 포함할 경우 동적인 조회가 어려울 수 있습니다. 특히, 카테고리별로 조회할 때 차단된 사용자의 게시글을 제외해야 하는 제약이 있는 경우, QueryDSL의 동적 쿼리 기능만으로는 충분하지 않을 수 있습니다. 이 경우 비즈니스 로직에 조건문을 추가하여 네이티브 쿼리를 사용하는 것이 현실적인 해결책이 될 수 있습니다.
참고
SQLExpressions (Querydsl 4.4.0 API)
cumeDist public static WithinGroup cumeDist(Object... args) As an aggregate function, CUME_DIST calculates, for a hypothetical row r identified by the arguments of the function and a corresponding sort specification, the relative position of row r amon
querydsl.com
JPA Querydsl from절에 subquery 사용하기(@Subselect 이용)
이슈사항 이번 내용도 배치로 통계로 구축하다가 생긴 이슈이다. 통계 쿼리를 mybatis로 작성하는 것은 쉬운데 검색해보니 querydsl은 subquery가 select 절이나 where 절에만 사용가능하고 from절에는 불
wms0603.tistory.com
'JPA' 카테고리의 다른 글
[JPA] QueryDsl 를 이용한 무한 페이징(Infinite scrolling) - 성능 개선 (0) | 2024.05.02 |
---|---|
[JPA] OSIV (1) | 2024.01.08 |
[JPA] 값 타입 (1) | 2024.01.05 |
[JPA] 프록시와 연관관계 관리 (0) | 2024.01.05 |