반응형
해당 글에서는 QueryDSL 환경에서 서브쿼리를 활용하는 방법에 대해 알아봅니다
💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 링크 |
Spring Boot Data JPA -1: ORM, JPA, Hibernate, QueryDSL 이론 | https://adjh54.tistory.com/421 |
Spring Boot Data JPA -2: 초기 환경 구성 + JpaRepository 활용 방법 | https://adjh54.tistory.com/422 |
Spring Boot Data JPA -3: 상세 JpaRepository 활용 방법(Query Method, @Query, NamedQuery) | https://adjh54.tistory.com/481 |
Spring Boot Data JPA 엔티티 어노테이션 -1 : 테이블 컬럼 단위 | https://adjh54.tistory.com/466 |
Spring Boot Data JPA 엔티티 어노테이션 -2 : 엔티티(테이블)간의 관계 | https://adjh54.tistory.com/477 |
Spring Boot Data JPA FetchType 이해하기 : 즉시/지연로딩 | https://adjh54.tistory.com/476 |
Spring Boot Data JPA + JPQL 활용 방법 | https://adjh54.tistory.com/479 |
Spring Boot Data JPA + Criteria API 활용 방법 | https://adjh54.tistory.com/483 |
Spring Boot Data JPA + QueryDSL 활용 방법-1 : 정의 및 구성요소 | https://adjh54.tistory.com/484 |
Spring Boot Data JPA + QueryDSL 활용 방법-2 : 초기 환경설정 및 활용예시 | https://adjh54.tistory.com/485 |
Spring Boot Data JPA + QueryDSL 활용 방법-3 : Join 활용하기 | https://adjh54.tistory.com/488 |
Spring Boot Data JPA + QueryDSL 활용 방법-4 : 서브쿼리 활용하기 | https://adjh54.tistory.com/489 |
1) QueryDSL(Domain-Specific Languages)
💡 QueryDSL
- 다양한 데이터베이스 플랫폼(RDBMS, No-SQL)에 접근하여 SQL과 유사한 문법으로 쿼리를 작성하여 데이터 처리를 수행하는데 도움을 주는 프레임워크입니다.
- 타입-세이프(type-safe)하게 쿼리를 작성하도록 지원하며 SQL 형태가 아닌 ‘자바 코드’로 작성하여 데이터베이스 쿼리 작성을 쉽고 안전하게 만들어줍니다.
2) 서브쿼리 메서드 : JPAExpressions, ExpressionUtils
1. JPAExpressions
💡 JPAExpressions
- QueryDSL의 도구 클래스 중 하나로 서브쿼리를 생성하는데 주로 사용됩니다. 이를 사용하면 쿼리 내에서 복잡한 식을 쉽게 표현할 수 있습니다.
- 타입-세이프한 서브쿼리를 만드는 데 필요한 여러 메서드를 제공하며, 이를 통해 동적인 쿼리를 만들 수 있습니다.
- 해당 클래스는 com.querydsl.jpa 패키지 내에 포함되어 있습니다..
💡 [참고] com.querydsl.jpa.JPAExpressions 클래스 메서드입니다.
메서드 | 리턴 타입 | 설명 |
avg(CollectionExpression<?,A> col) | static <A extends Comparable<? super A>> ComparableExpression<A> | 컬렉션 표현식의 평균 값을 반환합니다. |
max(CollectionExpression<?,A> left) | static <A extends Comparable<? super A>> ComparableExpression<A> | 컬렉션 표현식의 최대 값을 반환합니다. |
min(CollectionExpression<?,A> left) | static <A extends Comparable<? super A>> ComparableExpression<A> | 컬렉션 표현식의 최소 값을 반환합니다. |
select(Expression<?>... exprs) | static JPQLQuery<Tuple> | 주어진 표현식으로 새로운 JPQL 쿼리를 선택합니다. |
select(Expression<T> expr) | static <T> JPQLQuery<T> | 주어진 표현식을 선택하여 새로운 JPQL 쿼리를 생성합니다. |
selectDistinct(Expression<?>... exprs) | static JPQLQuery<Tuple> | 주어진 표현식으로 새로운 JPQL 쿼리를 선택하며, 중복된 결과를 제외합니다. |
selectDistinct(Expression<T> expr) | static <T> JPQLQuery<T> | 주어진 표현식을 선택하여 새로운 JPQL 쿼리를 생성하며, 중복된 결과를 제외합니다. |
selectFrom(EntityPath<T> expr) | static <T> JPQLQuery<T> | 엔티티 경로를 선택하여 새로운 JPQL 쿼리를 생성합니다. |
selectOne() | static JPQLQuery<Integer> | 결과 값이 하나인 JPQL 쿼리를 생성합니다. |
selectZero() | static JPQLQuery<Integer> | 결과 값이 0인 JPQL 쿼리를 생성합니다. |
treat(BeanPath<? extends T> path, Class<U> subtype) | static <U extends BeanPath<? extends T>,T>U | 주어진 경로를 하위 타입으로 변환합니다. |
type(EntityPath<?> path) | static StringExpression | 주어진 엔티티 경로의 타입을 문자열 표현식으로 반환합니다. |
💡 JPAExpression 클래스를 활용한 예시
queryFactory
.select(
Projections.fields(UserDto.class
, qUser.userId
, qUser.userNm
, qUser.userSt))
.from(qUser)
.where(qUser.userSq.in(
JPAExpressions
.select(qOrder.userSq)
.from(qOrder)
))
.fetch();
2. ExpressionUtils
💡 ExpressionUtils
- QueryDSL의 도구 클래스 중 하나로, 여러 표현식을 결합하거나 변환하는 데 사용됩니다. 이를 사용하면 복잡한 쿼리를 더 간결하게 표현할 수 있습니다.
- 타입-세이프한 방식으로 복잡한 쿼리를 만들 수 있습니다 .and, or, eq, ne, like 등의 메서드를 제공합니다.
- 해당 클래스는 com.querydsl.core.types 패키지 내에 포함되어 있습니다.
메서드 | 리턴 타입 | 설명 |
all(CollectionExpression<?,? super T> col) | static <T> Expression<T> | 주어진 col의 all 표현식을 생성합니다 |
all(SubQueryExpression<? extends T> col) | static <T> Expression<T> | 주어진 col의 all 표현식을 생성합니다 |
allOf(Predicate... exprs) | static @Nullable Predicate | 주어진 인수들의 교집합을 생성합니다 |
allOf(Collection<Predicate> exprs) | static @Nullable Predicate | 주어진 인수들의 교집합을 생성합니다 |
and(Predicate left, Predicate right) | static Predicate | 주어진 인수들의 교집합을 생성합니다 |
any(CollectionExpression<?,? super T> col) | static <T> Expression<T> | 주어진 col의 any 표현식을 생성합니다 |
any(SubQueryExpression<? extends T> col) | static <T> Expression<T> | 주어진 col의 any 표현식을 생성합니다 |
anyOf(Predicate... exprs) | static @Nullable Predicate | 주어진 인수들의 합집합을 생성합니다 |
anyOf(Collection<Predicate> exprs) | static @Nullable Predicate | 주어진 인수들의 합집합을 생성합니다 |
as(Expression<D> source, Path<D> alias) | static <D> Expression<D> | 주어진 source와 alias로 별칭 표현식을 생성합니다 |
as(Expression<D> source, String alias) | static <D> Expression<D> | 주어진 source와 alias로 별칭 표현식을 생성합니다 |
count(Expression<?> source) | static Expression<Long> | count(source) 표현식을 생성합니다 |
createRootVariable(Path<?> path) | static String | 주어진 경로를 기반으로 새로운 root 변수를 생성합니다 |
createRootVariable(Path<?> path, int suffix) | static String | 주어진 경로와 접미사를 기반으로 새로운 root 변수를 생성합니다 |
distinctList(Expression<?>... args) | static List<Expression<?>> | 주어진 args의 고유 리스트를 생성합니다 |
distinctList(Expression<?>[]... args) | static List<Expression<?>> | 연결된 배열의 내용의 고유 리스트를 생성합니다 |
eq(Expression<D> left, Expression<? extends D> right) | static <D> Predicate | left == right 표현식을 생성합니다 |
eqConst(Expression<D> left, D constant) | static <D> Predicate | left == constant 표현식을 생성합니다 |
extract(Expression<T> expr) | static <T> Expression<T> | 가능하면 래핑된 표현식을 가져옵니다 |
in(Expression<D> left, CollectionExpression<?,? extends D> right) | static <D> Predicate | left in right 표현식을 생성합니다 |
in(Expression<D> left, SubQueryExpression<? extends D> right) | static <D> Predicate | left in right 표현식을 생성합니다 |
in(Expression<D> left, Collection<? extends D> right) | static <D> Predicate | left in right 표현식을 생성합니다 |
inAny(Expression<D> left, Iterable<? extends Collection<? extends D>> lists) | static <D> Predicate | 각 리스트에 대한 left in right or... 표현식을 생성합니다 |
isNotNull(Expression<?> left) | static Predicate | left is not null 표현식을 생성합니다 |
isNull(Expression<?> left) | static Predicate | left is null 표현식을 생성합니다 |
likeToRegex(Expression<String> expr) | static Expression<String> | 주어진 like 패턴을 regex 패턴으로 변환합니다 |
likeToRegex(Expression<String> expr, boolean matchStartAndEnd) | static Expression<String> | 주어진 like 패턴을 regex 패턴으로 변환합니다 |
list(Class<T> clazz, Expression<?>... exprs) | static <T> Expression<T> | 주어진 인수들에 대한 리스트 표현식을 생성합니다 |
list(Class<T> clazz, List<? extends Expression<?>> exprs) | static <T> Expression<T> | 주어진 인수들에 대한 리스트 표현식을 생성합니다 |
ne(Expression<D> left, Expression<? super D> right) | static <D> Predicate | left != right 표현식을 생성합니다 |
neConst(Expression<D> left, D constant) | static <D> Predicate | left != constant 표현식을 생성합니다 |
notIn(Expression<D> left, CollectionExpression<?,? extends D> right) | static <D> Predicate | left not in right 표현식을 생성합니다 |
notIn(Expression<D> left, SubQueryExpression<? extends D> right) | static <D> Predicate | left not in right 표현식을 생성합니다 |
💡 ExpressionUtils를 활용한 예시
return queryFactory
.select(
Projections.fields(OrderDto.class,
ExpressionUtils
.as(JPAExpressions
.select(qOrderItem.price.multiply(qOrderItem.quantity).sum())
.from(qOrderItem)
.where(qOrderItem.orderSq.eq(qOrder.orderSq)
), "totalPrice"),
qOrder.orderDate)
)
.from(qOrder)
.where(qOrder.orderSq.eq(orderDto.getOrderSq()))
.fetchOne();
3) 서브쿼리(Scalar Subquery)
💡 서브쿼리(subquery)
- ‘하나의 SQL 문장 안에서 다른 SQL 문장을 사용하는 것'을 의
미합니다. 서브 쿼리는 메인 쿼리에 종속된 쿼리로 메인 쿼리의 결과로 필요로 할 때 사용합니다.
- 일반적으로 WHERE 또는 HAVING 절에서 사용되며 메인 쿼리와 서브쿼리 사이에는 ‘괄호’로 구분되며 메인 쿼리의 결과에 따라 다른 결과를 반환하는 데 사용됩니다.
사용 위치 | 서브쿼리 종류 |
SELECT | 스칼라 서브쿼리 |
FROM | 인라인 뷰 |
WHERE | 일반 서브쿼리, 스칼라 서브쿼리 |
HAVING | 일반 서브쿼리 |
4) 스칼라 서브쿼리(Scalar Subquery) : SELECT 절 내의 서브쿼리
💡 스칼라 서브쿼리(Scalar Subquery)
- 메인 쿼리의 ‘하나의 값(열, 컬럼)’으로 반환하며 SELECT 절에서 사용되는 서브쿼리를 의미합니다.
- QueryDSL 내에서는 ‘스칼라 서브쿼리’를 지원하지 않습니다. 대신 소스코드 내에서 이를 조합하여 처리하는 방식을 이용합니다.
1. 스칼라 서브쿼리(Scalar Subquery) 활용 테이블 간의 구조
💡 스칼라 서브쿼리(Scalar Subquery) 활용 테이블 간의 구조
- 주문 테이블(tb_order)과 주문 항목 테이블(tb_order_item)이 존재합니다.
- 하나의 주문은 여러 개의 주문 항목정보를 가질 수 있는 1 : N 관계의 구조입니다.
2. 스칼라 서브쿼리(Scalar Subquery) SQL 사용예시
💡 스칼라 서브쿼리(Scalar Subquery) SQL 사용예시
- 해당 SQL문은 주문 테이블(tb_order)과 주문 물품 테이블(tb_order_item)에 대한 관계에서 하나의 주문에 대한 주문 물품에 대한 합(가격 * 수량)을 가져오는 SQL문입니다.
SELECT (SELECT SUM(sub1.price * sub1.quantity)
FROM tb_order_item sub1
WHERE sub1.order_sq = t1.order_sq ) AS total_price
, t1.order_date
FROM tb_order t1
WHERE t1.order_sq = 1;
3. 스칼라 서브쿼리(Scalar Subquery) 코드 예시
💡 스칼라 서브쿼리(Scalar Subquery) 코드 예시 -1
- 데이터를 주고받는 DTO 구조입니다.
/**
* Please explain the class!!
*
* @author : lee
* @fileName : OrderDto
* @since : 2024. 5. 16.
*/
@Getter
@ToString
@NoArgsConstructor
public class OrderDto {
private long orderSq;
private Long userSq;
private Timestamp orderDate;
private int totalPrice;
@Builder(toBuilder = true)
public OrderDto(long orderSq, Long userSq, Timestamp orderDate, int totalPrice) {
this.orderSq = orderSq;
this.userSq = userSq;
this.orderDate = orderDate;
this.totalPrice = totalPrice;
}
}
💡 스칼라 서브쿼리(Scalar Subquery) 코드 예시 -2
- selectOrderSumItem() 메서드 내에서는 주문에 대한 합계를 구하는 메서드입니다.
- 서브쿼리 중 SELECT문에서 스칼라 서브쿼리로 수행이 되는 방법을 알아보기 위한 예시입니다.
1. Projections.fields()
- OrderDto.class 객체 형태로 반환받기 위해서 해당 Projection을 이용하였습니다.
2. ExpressionUtils.as()
- ExpressionUtils.as() 메서드는 주어진 source와 alias로 별칭 표현식을 생성합니다.
- 서브쿼리가 수행된 값을 "totalPrice"이라는 별칭으로 반환받기 위해 사용되었습니다.
3. JPAExpressions
- 실제 서브쿼리를 구성하는 방법입니다. qOrder 엔티티와 qOrderItem 엔티티 간의 관계를 통해서 서브쿼리를 구성하였습니다.
- price와 quantity의 곱(multiply)을 수행한 뒤 이 값을 합(sum)을 수행하는 방식으로 서브쿼리가 수행되었습니다.
package com.adjh.springbootquerydsl.dao.impl;
import com.adjh.springbootquerydsl.dao.OrderDao;
import com.adjh.springbootquerydsl.dto.OrderDto;
import com.adjh.springbootquerydsl.entity.QOrderEntity;
import com.adjh.springbootquerydsl.entity.QOrderItemEntity;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* Please explain the class!!
*
* @author : lee
* @fileName : OrderDaoImpl
* @since : 2024. 5. 16.
*/
@Repository
public class OrderDaoImpl implements OrderDao {
private final JPAQueryFactory queryFactory;
public OrderDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
private final QOrderItemEntity qOrderItem = QOrderItemEntity.orderItemEntity;
/**
* 주문에 대한 합계를 구합니다.
*
* @param orderDto
* @return
*/
@Override
@Transactional
public OrderDto selectOrderSumItem(OrderDto orderDto) {
OrderDto result = queryFactory
.select(
Projections.fields(OrderDto.class,
ExpressionUtils
.as(JPAExpressions
.select(qOrderItem.price.multiply(qOrderItem.quantity).sum())
.from(qOrderItem)
.where(qOrderItem.orderSq.eq(qOrder.orderSq)
), "totalPrice"),
qOrder.orderDate)
)
.from(qOrder)
.where(qOrder.orderSq.eq(orderDto.getOrderSq()))
.fetchOne();
return result;
}
}
4. 스칼라 서브쿼리(Scalar Subquery) 결과
💡 스칼라 서브쿼리(Scalar Subquery) 결과
- 위와 같은 처리 결과로 totalPrice 값을 반환받았습니다.
5) 인라인 뷰(Inline View) : FROM 절 내의 서브쿼리
💡 인라인 뷰(Inline View)
- 서브쿼리가 ‘하나의 테이블(가상 테이블)’로 만들어져 메인 쿼리에서 이를 사용하며 FROM절에서 사용되는 서브쿼리를 의미합니다.
- 새로운 가상의 테이블로 사용되기에 ‘AS(Alias)’를 통해 별칭을 지정해야 합니다.- QueryDSL 내에서는 FROM절 내에 직접적으로 서브쿼리를 사용하는 것은 불가능하다고 합니다.
- 그러나 Join을 이용하여 이를 구현하거나 혹은 Hibernate의 기능인 @Subselect 어노테이션을 통해서 수행하면 이를 대체할 수 있습니다.
1. 인라인 뷰(Inline View) 활용 테이블 간의 구조
💡 인라인 뷰(Inline View) 활용 테이블 간의 구조
- 주문 테이블(tb_order)과 주문 항목 테이블(tb_order_item)이 존재합니다.
- 하나의 주문은 여러 개의 주문 항목정보를 가질 수 있는 1 : N 관계의 구조입니다.
2. 인라인 뷰(Inline View) SQL 사용예시
💡 인라인 뷰(Inline View) SQL 사용예시
- 해당 SQL문은 주문 테이블(tb_order)과 주문 물품 테이블(tb_order_item)에 대한 관계에서 하나의 주문에 대해 가상의 테이블로 물품 테이블(tb_order_item)을 기반으로 주문 물품에 대한 합(가격 * 수량)을 가져와서 이를 불러오는 형태의 SQL문입니다
SELECT t1.order_sq, t1.order_date, sub1.total_price
FROM tb_order t1
JOIN (
SELECT SUM(price * quantity) AS total_price, order_sq
FROM tb_order_item
GROUP BY order_sq
) AS sub1
ON t1.order_sq = sub1.order_sq
WHERE t1.user_sq = 1;
3. 인라인 뷰(Inline View) 코드 예시
💡 인라인 뷰(Inline View) 코드 예시 -1
- 데이터를 주고받는 DTO 구조입니다.
/**
* Please explain the class!!
*
* @author : lee
* @fileName : OrderDto
* @since : 2024. 5. 16.
*/
@Getter
@ToString
@NoArgsConstructor
public class OrderDto {
private long orderSq;
private Long userSq;
private Timestamp orderDate;
private int totalPrice;
@Builder(toBuilder = true)
public OrderDto(long orderSq, Long userSq, Timestamp orderDate, int totalPrice) {
this.orderSq = orderSq;
this.userSq = userSq;
this.orderDate = orderDate;
this.totalPrice = totalPrice;
}
}
💡 인라인 뷰(Inline View) 코드 예시 -2
- selectOrderSumItem() 메서드 내에서는 주문에 대한 합계를 구하는 메서드입니다.
- 서브쿼리 중 FROM문에서 인라인 뷰로 수행되는 방법을 알아보기 위한 예시입니다.
(* 단, QueryDSL 내에서는 FROM 문 내에서는 서브쿼리를 구성할 수 없기에 JOIN문을 활용하여 구성합니다.)
1. Projections.fields()
- OrderDto.class 객체 형태로 반환받기 위해서 해당 Projection을 이용하였습니다.
2. join().on()
- FROM 절내에서 이를 수행할 수 없기에 Join을 통해서 주문 테이블(tb_order), 주문 항목 테이블(tb_order_item)에 대해 INNER JOIN을 수행하였습니다.
- 주문 테이블(tb_order)과 주문 항목 테이블(tb_order_item)은 1:N 관계로 order_sq값에 따라 두 테이블이 묶여 있습니다.
3. groupBy()
- 집계 함수로 sum()을 이용하기에 동일한 order_sq 값에 따라 groupBy를 수행합니다.
4. select()
- join이 수행된 결과에 따라서 가격과 수량의 곱에 따른 합을 가져옵니다.
package com.adjh.springbootquerydsl.dao.impl;
import com.adjh.springbootquerydsl.dao.OrderDao;
import com.adjh.springbootquerydsl.dto.OrderDto;
import com.adjh.springbootquerydsl.entity.QOrderEntity;
import com.adjh.springbootquerydsl.entity.QOrderItemEntity;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* Please explain the class!!
*
* @author : lee
* @fileName : OrderDaoImpl
* @since : 2024. 5. 16.
*/
@Repository
public class OrderDaoImpl implements OrderDao {
private final JPAQueryFactory queryFactory;
public OrderDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
private final QOrderItemEntity qOrderItem = QOrderItemEntity.orderItemEntity;
/**
* 주문에 대한 합계를 구합니다.
*
* @param orderDto
* @return
*/
@Override
@Transactional
public OrderDto selectOrderSumItem2(OrderDto orderDto) {
OrderDto result = queryFactory
.select(
Projections.fields(OrderDto.class,
qOrder.orderSq,
qOrder.orderDate,
qOrderItem.price.multiply(qOrderItem.quantity).sum().as("totalPrice")
)
)
.from(qOrder)
.join(qOrderItem).on(qOrder.orderSq.eq(qOrderItem.orderSq))
.where(qOrder.orderSq.eq(orderDto.getOrderSq()))
.groupBy(qOrder.orderSq)
.fetchOne();
return result;
}
}
4. 인라인 뷰(Inline View) 수행 결과
6) 일반 서브쿼리(Subquery) : WHERE절 / HAVING절 내의 서브쿼리
💡 일반 서브쿼리(Subquery)
- 메인 쿼리의 ‘조건’으로 사용이 되며 WHERE, HAVING절에서 사용되는 서브쿼리를 의미합니다.
1. 일반 서브쿼리(Subquery) 활용 테이블 간의 구조
💡 일반 서브쿼리(Subquery) 활용 테이블 간의 구조
- 사용자 테이블(tb_user)과 주문 테이블(tb_order)이 존재합니다.
- 사용자는 여러 번의 주문을 할 수 있는 1 : N 관계의 구조입니다.
2. 일반 서브쿼리(Subquery) SQL 사용예시
💡 일반 서브쿼리(Subquery) SQL 사용예시
- 해당 SQL문은 사용자 테이블(tb_user)과 주문 테이블(tb_order)에 대한 관계에서 사용자들 중 주문을 한 사용자가 있는지 확인을 하는 SQL문입니다.
SELECT t1.*
FROM tb_user t1
WHERE t1.user_sq IN (SELECT sub1.user_sq
FROM tb_order sub1);
3. 일반 서브쿼리(Subquery) 코드 예시
💡 일반 서브쿼리(Subquery) 코드 예시 -1
- 데이터를 주고받는 DTO 구조입니다.
/**
* Please explain the class!!
*
* @author : lee
* @fileName : UserDto
* @since : 4/23/24
*/
@Getter
@ToString
@NoArgsConstructor
public class UserDto {
private Long userSq;
private String userId;
private String userNm;
private String userSt;
@Builder(toBuilder = true)
public UserDto(Long userSq, String userId, String userNm, String userSt) {
this.userSq = userSq;
this.userId = userId;
this.userNm = userNm;
this.userSt = userSt;
}
}
💡 일반 서브쿼리(Subquery) 코드 예시 -2
- selectExistOrderUser() 메서드 내에서 주문한 사용자가 있는지 여부를 반환받는 메서드입니다.
- 서브쿼리 중 WHERE절에서 인라인 뷰로 수행되는 방법을 알아보기 위한 예시입니다.
1. Projections.fields()
- UserDto.class 객체 형태로 반환받기 위해서 해당 Projection을 이용하였습니다.
2. JPAExpressions
- 실제 서브쿼리를 구성하는 방법입니다. qOrder 엔티티에서 조회되는 userSq를 모두 반환받습니다.
- (다건)이 반환되는 값에 따라서 qUser.userSq.in()을 수행하여서 다건에 대한 처리를 수행합니다.
public class UserDaoImpl implements UserDao {
private final JPAQueryFactory queryFactory;
private final QUserEntity qUser = QUserEntity.userEntity;
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
@PersistenceContext
private EntityManager em;
public UserDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 주문한 사용자가 있는지 확인
*
* @param userDto
* @return
*/
@Override
public List<UserDto> selectExistOrderUser(UserDto userDto) {
return queryFactory
.select(
Projections.fields(UserDto.class
, qUser.userId
, qUser.userNm
, qUser.userSt))
.from(qUser)
.where(qUser.userSq.in(
JPAExpressions
.select(qOrder.userSq)
.from(qOrder)
))
.fetch();
}
}
4. 일반 서브쿼리(Subquery) 수행 결과
💡 일반 서브쿼리(Subquery) 수행 결과
- 위와 같은 처리 결과로 주문한 사용자의 리스트를 반환받았습니다.
7) @Subselect : FROM 절 서브쿼리 대안
💡 @Subselect
- Hibernate에서 제공하는 어노테이션으로 엔티티를 하위 쿼리 결과로 매핑하는 데 사용됩니다.
- 이 어노테이션은 보통 가상의 뷰를 표현하는 데 사용되며, 엔티티가 바로 SQL 하위 쿼리에 매핑됩니다. 이 하위 쿼리는 데이터베이스에 물리적인 뷰가 없어도 됩니다.
- 대신, Hibernate는 이 어노테이션을 사용하여 논리적인 뷰를 생성하고, 이 논리적인 뷰를 기반으로 엔티티를 매핑합니다.
1. @Subselect를 활용 테이블 간의 구조
💡 @Subselect를 활용 테이블 간의 구조
- 주문 테이블(tb_order)과 주문 항목 테이블(tb_order_item)이 존재합니다.
- 하나의 주문은 여러 개의 주문 항목정보를 가질 수 있는 1 : N 관계의 구조입니다.
2. @Subselec의 SQL 사용예시
💡 @Subselec의 SQL 사용예시
- 해당 SQL문은 주문 테이블(tb_order)과 주문 물품 테이블(tb_order_item)에 대한 관계에서 하나의 주문에 대해 가상의 테이블로 물품 테이블(tb_order_item)을 기반으로 주문 물품에 대한 합(가격 * 수량)을 가져와서 이를 불러오는 형태의 SQL문입니다
SELECT t1.order_sq, t1.order_date, sub1.total_price
FROM tb_order t1
JOIN (
SELECT SUM(price * quantity) AS total_price, order_sq
FROM tb_order_item
GROUP BY order_sq
) AS sub1
ON t1.order_sq = sub1.order_sq
WHERE t1.user_sq = 1;
3. @Subselect를 코드 예시
💡 @Subselect를 코드 예시 -1
1. @Subselect
- subquery로 사용될 SQL문을 Native Query 형태로 작성을 합니다.
2. @Immutable
- @Immutable 어노테이션은 해당 엔티티가 변경 불가능하다는 것을 Hibernate에게 알려줍니다.
- OrderItemSubEntity는 서브쿼리를 위한 가상 VIEW로 사용되므로 데이터가 변경되어서는 안 되기 때문에 @Immutable을 사용하였습니다.
package com.adjh.springbootquerydsl.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Subselect;
/**
* OrderItem 테이블의 SubQuery를 위한 가상 VIEW
*
* @author : lee
* @fileName : OrderItemSubEntity
* @since : 2024. 5. 17.
*/
@Entity
@Subselect(
"SELECT SUM(price * quantity) AS total_price, order_sq " +
"FROM tb_order_item " +
"GROUP BY order_sq "
)
@Immutable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItemSubEntity {
@Id
@Column(name = "order_sq")
@Comment("주문 시퀀스")
private Long orderSq;
@Column(name = "total_price")
@Comment("총 가격 합계")
private int totalPrice;
public OrderItemSubEntity(Long orderSq, int totalPrice) {
this.orderSq = orderSq;
this.totalPrice = totalPrice;
}
}
[ 더 알아보기 ]
💡@Entity로 어노테이션을 사용하였는데 그러면 테이블도 생성되는 게 아닐까?
- @Subselect 어노테이션이 붙은 엔티티는 실제 데이터베이스 테이블에 매핑되지 않으며, 하이버네이트가 논리적인 뷰로써 이를 관리합니다. 따라서 실제 데이터베이스에 새로운 테이블이 생성되지 않습니다.
💡@Immutable은 어디에 사용되는 걸까?
- 해당 엔티티가 변경 불가능(immutable)하다는 것을 나타냅니다. 즉, 한 번 생성된 후에는 수정될 수 없습니다. @Immutable이 붙은 클래스에 대한 변경 사항은 데이터베이스에 반영되지 않으므로, 이를 사용하면 데이터베이스와의 동기화를 걱정하지 않고 객체를 사용할 수 있습니다.
@Repository
public class OrderDaoImpl implements OrderDao {
private final JPAQueryFactory queryFactory;
public OrderDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
private final QOrderItemEntity qOrderItem = QOrderItemEntity.orderItemEntity;
private final QOrderItemSubEntity qOrderItemSubEntity = QOrderItemSubEntity.orderItemSubEntity;
/**
* 주문에 대한 합계를 구합니다.
*
* @param orderDto
* @return
*/
@Override
public OrderDto selectOrderSumItem3(OrderDto orderDto) {
return queryFactory
.select(
Projections.fields(OrderDto.class,
qOrder.orderSq,
qOrder.orderDate,
qOrderItemSubEntity.totalPrice
)
)
.from(qOrder, qOrderItemSubEntity)
.fetchOne();
}
}
4. @Subselect 수행 결과
💡 @Subselect 수행 결과
- 위와 같은 처리 결과로 주문에 대한 합계 값을 받았습니다.
오늘도 감사합니다. 😀
반응형
'Java > JPA' 카테고리의 다른 글
[Java/JPA] Spring Boot JPA 환경에서 HikariCP 적용하기 (0) | 2024.07.22 |
---|---|
[Java/JPA] Spring Boot Data JPA + QueryDSL 이해하기 -3: Join 활용(내부, 외부, 패치, 세타) (0) | 2024.05.11 |
[Java/JPA] Spring Boot Data JPA + QueryDSL 이해하기 -2 : 초기 환경설정 및 사용예시 (0) | 2024.05.05 |
[Java/JPA] Spring Boot Data JPA + QueryDSL 이해하기 -1 : 정의 및 구성요소 (0) | 2024.05.04 |
[Java/JPA] Spring Boot Data JPA + Criteria API 이해하기 -1 : 정의 및 기본동작 (0) | 2024.04.24 |