관리자 측면에서 등록된 상품 리스트를 조회할 수 있는 관리 기능
상품을 조회하는 조건을 설정하고 페이징 기능을 통해 일정 개수의 상품을 불러오고, 상품 상세 페이지로 이동할 수 있는 기능까지 갖춘 관리 기능을 만들었다.
조회 조건으로는
와 같은 값을 세팅했다. 조회 조건이 여러가지이고 복잡하기 때문에 Querydsl
을 이용해서 쿼리를 동적으로 생성하기로 했다. Querydsl을 사용하면 비슷한 쿼리를 재사용할수 있다는 장점이 있기 때문에 여러모로 사용하는것이 편리한 것 같다. Querydsl을 사용하기 위해서 Maven의 컴파일 명령을 통해 QDomain 클래스를 생성하였다.
컴파일을 하고 나면 다음과 같이 target 폴더 아래 QDomain 클래스들이 생성된다.
상품을 조회하기 위해 조회 조건을 가진 Dto 클래스를 먼저 만들어야한다.
ItemSearchDto.java
package com.shop.dto;
import com.shop.constant.ItemSellStatus;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class ItemSearchDto {
private String searchDateType;
private ItemSellStatus searchSellStatus;
private String searchBy;
private String searchQuery = "";
}
searchDateType
: 현재 시간과 상품 등록일을 비교하여 상품 데이터를 조회.searchSellStatus
: 상품의 판매 상태를 기준으로 상품 데이터를 조회searchBy
: 상품을 조회할 때 어떤 유형으로 조회할지 선택itemNm
: 상품명createdBy
: 상품 등록자 아이디searchQuery
: 조회할 검색어를 저장하는 변수Querydsl을 Spring Data Jpa와 함께 사용하기 위해서는 사용자 정의 리포지토리를 생성해야한다. 다음 3단계를 거친다.
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- Spring Data Jpa Repository 에서 사용자 정의 인터페이스 상속
ItemRepositoryCustom.java
package com.shop.repository;
import com.shop.dto.ItemSearchDto;
import com.shop.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface ItemRepositoryCustom {
Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
}
itemSearchDto
객체와 페이징 정보를 담고 있는 Pageable 객체를 파라미터로 받는 getAdminItemPage
메소드를 정의.ItemRepositoryCustomImpl.java
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {
// 1.
private JPAQueryFactory queryFactory;
// 2.
public ItemRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
// 3.
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) {
// 4.
return searchSellStatus == null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
// 4.
}
private BooleanExpression regDtsAfter(String searchDateType) {
// 5.
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 QItem.item.regTime.after(dateTime);
}
private BooleanExpression searchByLike(String searchBy, String searchQuery) {
// 6.
if(StringUtils.equals("itemNM", searchBy)) {
return QItem.item.itemNm.like("%" + searchQuery + "%");
} else if (StringUtils.equals("createdBy", searchBy)) {
return QItem.item.createdBy.like("%" + searchQuery + "%");
}
return null;
}
@Override
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
List<Item> content = queryFactory // 7.
.selectFrom(QItem.item)
.where(regDtsAfter(itemSearchDto.getSearchDateType()),
searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
searchByLike(itemSearchDto.getSearchBy(),
itemSearchDto.getSearchQuery()))
.orderBy(QItem.item.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory.select(Wildcard.count).from(QItem.item)
.where(regDtsAfter(itemSearchDto.getSearchDateType()),
searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
.fetchOne()
;
return new PageImpl<>(content, pageable, total);
// 8.
}
ItemRepositoryCustom
상속
동적 쿼리 생성을 위해 JPAQueryFactory 클래스 사용
JPAQueryFactory 생성자로 EntityManager
객체를 넣어줌
상품 판매 상태 조건이 null 일 경우, null 리턴. 결과값이 null이면 where 절에서 해당 조건은 무시됨. (판매중 or 품절 상태일때만 조회)
SearchDateType
의 값에 따라서 dateTime의 값을 이전 시간의 값으로 세팅 후, 해당 시간 이후로 등록된 상품만 조회.
(ex. searchDateType이 "1m"인 경우, dateTime의 시간을 한 달 전으로 세팅 후 최근 한 달 동안 등록된 상품만 조회)
searchBy의 값에 따라서 상품명에 검색어를 포함하고 있는 상품 또는 상품 생성자의 아이디에 검색어를 포함하고 있는 상품을 조회하도록 조건 값을 반환
queryFactory
를 이용해서 쿼리 생성.
selectFrom(QItem.item)
: 상품 데이터를 조회하기 위해 QItem의 item을 지정where 조건절
: BooleanExpresion 반환하는 조건문을 넣어줌. offset
: 데이터를 가지고 올 시작 인덱스를 지정limit
: 한 번에 가지고 올 최대 개수를 지정fetchResult()
: 조회한 리스트 및 전체 개수를 포함하는 QueryResults
를 반환. 상품 데이터 리스트 조회 및 상품 데이터 전체 개수를 조회하는 2번의 쿼리문이 실행됨조회한 데이터를 Page 클래스의 구현체인 PageImpl 객체로 반환
이제, ItemRepository
인터페이스에서 ItemRepositoryCustom
인터페이스를 상속한다. 이렇게 하면 ItemRepository
에서 Querydsl로 구현한 상품 관리 페이지 목록을 불러오는 getAdminItemPage() 메소드를 사용할 수 있다.
ItemRepository.java
package com.shop.repository;
// ..import 생략
public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item>, ItemRepositoryCustom {
// 코드 생략
}
ItemService
클래스에 상품 조회 조건과 페이지 정보를 파라미터로 받아서 상품 데이터를 조회하는 getAdminItemPage()
메소드를 추가한다. 데이터의 수정이 일어나지는 않으므로 Transactional(readOnly = true)
어노테이션을 설정했다.
ItemService.java
package com.shop.service;
// import 생략
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {
// ..코드 생략
@Transactional(readOnly = true)
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){
return itemRepository.getAdminItemPage(itemSearchDto, pageable);
}
}
마지막으로 ItemController
클래스에 상품 관리 화면 이동 및 조회한 상품 데이터를 화면에 전달하는 로직을 구현했다.
ItemController.java
package com.shop.controller;
// ..import 생략
@Controller
@RequiredArgsConstructor
public class ItemController {
// ..코드 생략
@GetMapping(value = {"/admin/items", "/admin/items/{page}"})
// 1.
public String itemManage(ItemSearchDto itemSearchDto,
@PathVariable("page") Optional<Integer> page, Model model) {
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 3);
// 2.
Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);
// 3.
model.addAttribute("items", items); // 4.
model.addAttribute("itemSearchDto", itemSearchDto); // 5.
model.addAttribute("maxPage", 5); // 6.
return "item/itemMng";
}
PageRequest.of
메소드를 통해 Pageable
객체 생성. URL 경로에 페이지 번호가 있으면 해당 페이지를 조회하도록 세팅.📌
PageRequest.of
메소드의 파라미터 의미
첫 번째 파라미터 : 조회할 때 페이지 번호
두 번째 파라미터 : 한 번에 가지고 올 데이터 수