From 3b677f1de1be142d8ba82213b8cadf0f4f89ee36 Mon Sep 17 00:00:00 2001 From: boxinthechaos <166705762+boxinthechaos@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:43:25 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=BC=EC=A0=9C=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + .../order/repository/OrderJpaRepository.java | 31 +---- .../custom/OrderRepositoryCustom.java | 10 ++ .../impl/OrderRepositoryCustomImpl.java | 111 ++++++++++++++++++ .../task28/global/config/QueryDslConfig.java | 4 +- 5 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/OrderRepositoryCustom.java create mode 100644 src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/impl/OrderRepositoryCustomImpl.java diff --git a/build.gradle.kts b/build.gradle.kts index 85b5425..f45c2ce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,9 @@ dependencies { // QueryDSL implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta") annotationProcessor("com.querydsl:querydsl-apt:5.1.0:jakarta") + annotationProcessor("jakarta.persistence:jakarta.persistence-api:3.1.0") + annotationProcessor("jakarta.annotation:jakarta.annotation-api:2.1.1") + implementation("jakarta.persistence:jakarta.persistence-api:3.1.0") // Lombok compileOnly("org.projectlombok:lombok") diff --git a/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/OrderJpaRepository.java b/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/OrderJpaRepository.java index b34e0cd..dc32c02 100644 --- a/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/OrderJpaRepository.java +++ b/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/OrderJpaRepository.java @@ -1,8 +1,10 @@ package com.gsm._8th.class4.backend.task28.domain.order.repository; import com.gsm._8th.class4.backend.task28.domain.order.entity.OrderJpaEntity; +import com.gsm._8th.class4.backend.task28.domain.order.repository.custom.OrderRepositoryCustom; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,35 +12,8 @@ @Repository -public interface OrderJpaRepository extends JpaRepository { +public interface OrderJpaRepository extends JpaRepository, OrderRepositoryCustom { - /** - * 주문 검색 메서드 (JPQL 기반 - QueryDSL 마이그레이션 대상) - * - *

- * 이 메서드는 사용자 ID, 가격 범위, 주소, 상품명 등 다양한 조건에 따라 주문을 조회하는 예시입니다.
- * 내부적으로 주문 → 주문 아이템 → 상품(Item)까지 JOIN하여 필터링하며, 결과는 페이징(Pageable) 처리됩니다. - *

- * - *

- * ⚠️ 본 메서드는 의도적으로 N+1 문제가 발생하도록 설계되었습니다.
- * 즉, 연관 엔티티인 orderItemsitem은 Lazy 로딩되며, - * 실제 조회 시 각 주문마다 추가 쿼리가 발생합니다. - *

- * - *

- * 이 메서드는 QueryDSL 기반의 동적 쿼리로 마이그레이션해야 하며, - * 필요 시 fetch join 또는 @EntityGraph 등을 통해 N+1 문제를 해결하도록 유도합니다. - *

- * - * @param userId 검색할 사용자 ID - * @param minPrice 최소 주문 가격 - * @param maxPrice 최대 주문 가격 - * @param address 포함되어야 할 배송지 문자열 (LIKE 검색) - * @param itemName 포함되어야 할 상품명 문자열 (LIKE 검색) - * @param pageable 페이징 및 정렬 정보를 담는 Pageable 객체 - * @return 조건에 맞는 주문 목록을 포함한 Page 객체 - */ @Query( "SELECT DISTINCT o FROM OrderJpaEntity o " + "JOIN o.orderItems oi " + diff --git a/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/OrderRepositoryCustom.java b/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/OrderRepositoryCustom.java new file mode 100644 index 0000000..577a9f9 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/OrderRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.gsm._8th.class4.backend.task28.domain.order.repository.custom; + +import com.gsm._8th.class4.backend.task28.domain.order.entity.OrderJpaEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + + +public interface OrderRepositoryCustom { + Page searchOrders(Long userId, Integer minPrice, Integer maxPrice, String address, String itemName, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/impl/OrderRepositoryCustomImpl.java b/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/impl/OrderRepositoryCustomImpl.java new file mode 100644 index 0000000..6d8bbeb --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task28/domain/order/repository/custom/impl/OrderRepositoryCustomImpl.java @@ -0,0 +1,111 @@ +package com.gsm._8th.class4.backend.task28.domain.order.repository.custom.impl; + +import com.gsm._8th.class4.backend.task28.domain.order.entity.OrderJpaEntity; +import com.gsm._8th.class4.backend.task28.domain.order.repository.custom.OrderRepositoryCustom; +import com.gsm._8th.class4.backend.task28.domain.order.entity.QOrderJpaEntity; +import com.gsm._8th.class4.backend.task28.domain.item.entity.QItemJpaEntity; +import com.gsm._8th.class4.backend.task28.domain.orderItem.entity.QOrderItemJpaEntity; +import com.gsm._8th.class4.backend.task28.domain.user.entity.QUserJpaEntity; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Repository +@RequiredArgsConstructor +public class OrderRepositoryCustomImpl implements OrderRepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public Page searchOrders(Long userId, Integer minPrice, Integer maxPrice, String address, String itemName, Pageable pageable){ + QOrderJpaEntity orderJpaEntity = QOrderJpaEntity.orderJpaEntity; + QOrderItemJpaEntity orderItemJpaEntity = QOrderItemJpaEntity.orderItemJpaEntity; + QItemJpaEntity itemJpaEntity = QItemJpaEntity.itemJpaEntity; + QUserJpaEntity userJpaEntity = QUserJpaEntity.userJpaEntity; + + List orders = queryFactory + .select(orderJpaEntity) + .distinct() + .leftJoin(orderJpaEntity.orderItems, orderItemJpaEntity).fetchJoin() + .leftJoin(orderItemJpaEntity.item, itemJpaEntity).fetchJoin() + .leftJoin(orderJpaEntity.user, userJpaEntity).fetchJoin() + .where( + eqUserId(userId, orderJpaEntity), + goeMinPrice(minPrice, orderJpaEntity), + loeMaxPrice(maxPrice, orderJpaEntity), + containsAddress(address, orderJpaEntity), + containsItemName(itemName, itemJpaEntity) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long totalCount = queryFactory + .select(orderJpaEntity.count()) + .from(orderJpaEntity) + .where( + eqUserId(userId, orderJpaEntity), + goeMinPrice(minPrice, orderJpaEntity), + loeMaxPrice(maxPrice, orderJpaEntity), + containsAddress(address, orderJpaEntity), + containsItemName(itemName, itemJpaEntity) + ) + .fetchOne(); + + long total = (totalCount != null) ? totalCount : 0L; + return new PageImpl<>(orders,pageable,total); + } + + private BooleanExpression eqUserId(Long userId, QOrderJpaEntity orderJpaEntity) { + if (userId != null) { + return orderJpaEntity.user.id.eq(userId); + } else { + return Expressions.asBoolean(true).isTrue(); + } + } + + private BooleanExpression goeMinPrice(Integer minPrice, QOrderJpaEntity orderJpaEntity) { + if (minPrice != null){ + return orderJpaEntity.price.goe(minPrice); + } + else{ + return Expressions.asBoolean(true).isTrue(); + } + } + + private BooleanExpression loeMaxPrice(Integer maxPrice, QOrderJpaEntity orderJpaEntity) { + if (maxPrice != null){ + return orderJpaEntity.price.loe(maxPrice); + } + else{ + return Expressions.asBoolean(true).isTrue(); + } + } + + private BooleanExpression containsAddress(String address, QOrderJpaEntity orderJpaEntity) { + if (address != null){ + return orderJpaEntity.address.contains(address); + } + else{ + return Expressions.asBoolean(true).isTrue(); + } + } + + private BooleanExpression containsItemName(String itemName, QItemJpaEntity itemJpaEntity) { + if (itemName != null){ + return itemJpaEntity.name.contains(itemName); + } + else{ + return Expressions.asBoolean(true).isTrue(); + } + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task28/global/config/QueryDslConfig.java b/src/main/java/com/gsm/_8th/class4/backend/task28/global/config/QueryDslConfig.java index 37af08a..dfaf601 100644 --- a/src/main/java/com/gsm/_8th/class4/backend/task28/global/config/QueryDslConfig.java +++ b/src/main/java/com/gsm/_8th/class4/backend/task28/global/config/QueryDslConfig.java @@ -1,11 +1,9 @@ package com.gsm._8th.class4.backend.task28.global.config; - import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - @Configuration public class QueryDslConfig { @PersistenceContext @@ -13,6 +11,6 @@ public class QueryDslConfig { @Bean public JPAQueryFactory jpaQueryFactory() { - return new JPAQueryFactory(entityManager); + return new JPAQueryFactory(() -> (jakarta.persistence.EntityManager) entityManager); } } \ No newline at end of file