- ‘하나의 SQL 문장 안에서 다른 SQL 문장을 사용하는 것'을 의 미합니다. 서브 쿼리는 메인 쿼리에 종속된 쿼리로 메인 쿼리의 결과로 필요로 할 때 사용합니다. - 일반적으로 WHERE 또는 HAVING 절에서 사용되며 메인 쿼리와 서브쿼리 사이에는 ‘괄호’로 구분되며 메인 쿼리의 결과에 따라 다른 결과를 반환하는 데 사용됩니다.
- 메인 쿼리의 ‘하나의 값(열, 컬럼)’으로 반환하며 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)을 수행하는 방식으로 서브쿼리가 수행되었습니다.
- 서브쿼리가 ‘하나의 테이블(가상 테이블)’로 만들어져 메인 쿼리에서 이를 사용하며 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이 수행된 결과에 따라서 가격과 수량의 곱에 따른 합을 가져옵니다.
- 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();
}
}