Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,19 @@
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;
import org.springframework.stereotype.Repository;


@Repository
public interface OrderJpaRepository extends JpaRepository<OrderJpaEntity, Long> {
public interface OrderJpaRepository extends JpaRepository<OrderJpaEntity, Long>, OrderRepositoryCustom {

/**
* 주문 검색 메서드 (JPQL 기반 - QueryDSL 마이그레이션 대상)
*
* <p>
* 이 메서드는 사용자 ID, 가격 범위, 주소, 상품명 등 다양한 조건에 따라 주문을 조회하는 예시입니다.<br>
* 내부적으로 주문 → 주문 아이템 → 상품(Item)까지 JOIN하여 필터링하며, 결과는 페이징(Pageable) 처리됩니다.
* </p>
*
* <p>
* ⚠️ 본 메서드는 의도적으로 <b>N+1 문제</b>가 발생하도록 설계되었습니다.<br>
* 즉, 연관 엔티티인 <code>orderItems</code> 및 <code>item</code>은 Lazy 로딩되며,
* 실제 조회 시 각 주문마다 추가 쿼리가 발생합니다.
* </p>
*
* <p>
* 이 메서드는 <b>QueryDSL 기반의 동적 쿼리로 마이그레이션</b>해야 하며,
* 필요 시 <code>fetch join</code> 또는 <code>@EntityGraph</code> 등을 통해 N+1 문제를 해결하도록 유도합니다.
* </p>
*
* @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 " +
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OrderJpaEntity> searchOrders(Long userId, Integer minPrice, Integer maxPrice, String address, String itemName, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -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<OrderJpaEntity> 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<OrderJpaEntity> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
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
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
return new JPAQueryFactory(() -> (jakarta.persistence.EntityManager) entityManager);
}
}