Jin's Dev Story

[스프링 부트 쇼핑몰 프로젝트 with JPA] 8-1. [상품 관리] 상품 목록 조회 Querydsl 본문

Web & Android/스프링 부트 쇼핑몰 프로젝트 with JPA

[스프링 부트 쇼핑몰 프로젝트 with JPA] 8-1. [상품 관리] 상품 목록 조회 Querydsl

woojin._. 2023. 10. 16. 18:20
이 내용은 스프링 부트 쇼핑몰 프로젝트 with JPA 책을 학습한 내용입니다.

1. Qdomain 생성

  • Querydsl을 사용하기 위해서 Qdomain 생성

  • dependencies 부분 추가

  • build.gradle - [other] - [compileQuerydsl]

2. 상품 조회 조건 DTO

  • 상품 조회 조건
    • 상품 등록일
    • 상품 판매 상태
    • 상품명 또는 상품 등록자 아이디
  • 상품 조회 조건을 담을 ItemSearchDto 클래스 생성
import kr.spring.item.constant.ItemSellStatus;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class ItemSearchDto {

    private String searchDateType;

    private ItemSellStatus searchSellStatus;

    private String searchBy;  // 방법(사람이름인지 상품 이름인지)

    private String searchQuery; // 검색
}

3. Querydsl & Spring Data Jpa

  • Querydsl과 Spring Data Jpa를 함께 사용하기 위해서는 사용자 정의 리포지토리가 필요함② 사용자 정의 인터페이스 구현
  • ③ Spring Data Jpa 리포지토리에서 사용자 정의 인터페이스 상속
  • ① 사용자 정의 인터페이스 작성

4. 사용자 정의 인터페이스 작성

  • 상품 조회 조건을 담고 있는 itemSearchDto 객체와 페이징 정보를 담고 있는 pageable 객체를 파라미터로 받고, Page<Item> 객체를 반환
import kr.spring.item.dto.ItemMainDto;
import kr.spring.item.dto.ItemSearchDto;
import kr.spring.item.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface ItemRepositoryCustom {

    // 몇 번째 페이지부터 몇 개를 가져올 건지
    Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable);

}

5. 사용자 정의 인터페이스 구현

  • 클래스명 끝에 "Impl" 를 붙여야 정상적으로 동작함
  • 클래스 생성 및 사용자 정의 인터페이스 구현
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {

    private JPAQueryFactory queryFactory;

    public ItemRepositoryCustomImpl(EntityManager em) {
        queryFactory = new JPAQueryFactory(em);
    }
}
  • BooleanExpression 을 통해 where 절에 적용될 조회 조건을 생성
  • BooleanExpresiion 값이 null 이면 해당 조회 조건을 사용하지 않겠다는 의미 (=all)
  • 상품 등록일 조건
// ItemRepositoryCustomImpl 
private BooleanExpression regDtsAfter(String searchDateType) {

        LocalDateTime dateTime = LocalDateTime.now();

        if(StringUtils.equals("all", searchDateType) || searchDateType == null) {
            return null;
        } else if(StringUtils.equals("1d", searchDateType)) {
            dateTime = dateTime.minusDays(1);
        } else if(StringUtils.equals("1w", searchDateType)) {
            dateTime = dateTime.minusWeeks(1);
        } else if(StringUtils.equals("1m", searchDateType)) {
            dateTime = dateTime.minusMonths(1);
        } else if(StringUtils.equals("6m", searchDateType)) {
            dateTime = dateTime.minusMonths(6);
        }

        return item.regTime.after(dateTime);
    }
  • 상품 판매 상태 조건
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) {

        return searchSellStatus == null ? null : item.itemSellStatus.eq(searchSellStatus);
    }
  • 상품명 또는 상품 등록자 아이디 조건
private BooleanExpression searchByLike(String searchBy, String searchQuery) {

        // 방법에 따라 다름
        // 상품명에 따라 검색
        if(StringUtils.equals("itemNm", searchBy)) {
            return item.itemNm.like("%" + searchQuery + "%");
        }
        // 작성자에 따라 검색
        else if (StringUtils.equals("createdBy", searchBy)) {
            return item.createdBy.like("%" + searchQuery + "%");
        }

        return null;
    }
  • QueryFactory 를 이용하여 Querydsl 쿼리문 생성offset : 데이터를 가지고 올 시작 인덱스를 지정limit : 한 번에 가지고 올 최대 개수를 지정
  • fetchResult() 메소드를 이용하여 조회 대상 리스트 및 전체 개수를 포함하는 QueryResults 객체 반환
  • Page 클래스의 구현체인 PageImpl 객체로 반환
@Override
    public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {

        List<Item> content = queryFactory
                .selectFrom(item)
                .where(regDtsAfter(itemSearchDto.getSearchDateType()),
                        searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
                        // 어떤 방법으로 어떤 디비를 던질 것인지
                        searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
                .orderBy(item.id.desc())
                .offset(pageable.getOffset()) // 시작할 위치
                .limit(pageable.getPageSize()) // 가져올 갯수
                .fetch(); // 리스트를 가져옴

        long total = queryFactory.select(Wildcard.count).from(item)
                                .where(regDtsAfter(itemSearchDto.getSearchDateType()),
                                    searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
                                    searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
                                .fetchOne();

        return new PageImpl<>(content, pageable, total);
    }

6. 사용자 정의 인터페이스 상속

  • JpaRepository 를 구현한 ItemRepository 에서 ItemRepositoryCustom을 상속
public interface ItemRepository extends JpaRepository<Item, Long>,
        QuerydslPredicateExecutor<Item>, ItemRepositoryCustom {

7. ItemService 수정

  • 위에서 만든 사용자 정의 조회문(=메소드)을 수행하는 로직 추가
  • 조회 기능이므로 읽기 전용 상태로 지정
// 정보 불러오기
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {

     return itemRepository.getAdminItemPage(itemSearchDto, pageable);

}

8. ItemController 수정

  • Mapping 파라미터로 객체를 지정하면(ItemSearchDto) 자동으로 new 객체 생성
  • URL을 통해 페이지 번호가 넘어오는 경우 @PathVariable로 변수 값 매핑
// 요청 URL에 페이지 번호가 없는 경우와 있는 경우 2가지를 매핑
    @GetMapping({"/admin/items", "/admin/items/{page}"})
    public String itemList(ItemSearchDto itemSearchDto, Model model,
                           @PathVariable("page")Optional<Integer> page) {

        // 페이지의 내용이 있으면 페이지 번호(page.get())을 가져오고 없으면 0으로 가져옴
        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 10);  // 한페이지에 표시되는 상품의 수를 관리(현재는 3개씩 보여줌)
        Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);

        model.addAttribute("items", items);
        // 검색어 다시 받아오기
        model.addAttribute("itemSearchDto", itemSearchDto);
        // View 단에서 하단에 보여줄 페이지 번호의 최대 개수 설정
        model.addAttribute("maxPage", 5);

        return "item/itemList";
    }