상품 관리 화면에서는 상품을 조회하는 조건을 설정 후 페이징 기능을 통해 일정 개수의 상품만 불러오게 구현
- 조회조건)
1) 상품 등록일
2) 상푸 판매 상태
3) 상품명 또는 상품 등록자 아이디
이렇게 조회 조건이 복잡한 화면은 Querydsl을 이용해 조건에 맞는 쿼리를 동적으로 쉽게 생성하게 한다.
1. 컴파일 진행후 소스파일로 인식
2) DTO 생성
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;
// 상품을 조회할때 어떤 유형으로 조회할지 선택
// ex) itemNm : 상품명, createBy : 상품 등록자 아이디
private String searchBy;
// 조회할 검색어 저장할 변수
private String searchQuery = "";
}
3) 인터페이스 생성
package com.shop.repository;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
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);
}
4) 인터페이스 구현
package com.shop.repository;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.dto.QMainItemDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.entity.QItem;
import com.shop.entity.QItemImg;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.thymeleaf.util.StringUtils;
import javax.persistence.EntityManager;
import java.time.LocalDateTime;
import java.util.List;
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{
private JPAQueryFactory queryFactory;
public ItemRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus){
return searchSellStatus == null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
}
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 QItem.item.regTime.after(dateTime);
}
private BooleanExpression searchByLike(String searchBy, String searchQuery){
if(StringUtils.equals("itemNm", searchBy)){
return QItem.item.itemNm.like("%"+searchQuery+"%");
} else if(StringUtils.equals("createBy",searchBy)){
return QItem.item.createBy.like("%"+searchQuery+"%");
}
return null;
}
@Override
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
// https://toongri.tistory.com/62
// Querydsl 5.0.0 : fetchResults(), fetchCount() are deprecated
List<Item> fetch = queryFactory
.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();
JPAQuery<Item> count = queryFactory
.selectFrom(QItem.item)
.where(regDtsAfter(itemSearchDto.getSearchDateType()),
searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()));
return PageableExecutionUtils.getPage(fetch, pageable, ()-> count.fetch().size());
}
}
- BooleanExpression
일반적으로 동적쿼리를 사용할 때 BooleanBuilder를 사용한다. 사실상 이를 사용해도 큰 문제가 되는 것은 없으나 어떤 쿼리인지 바로 예측이 가지 않는다는 단점이 있다.
BooleanExpression은 null 반환 시 자동으로 조건절에서 제거 된다. (단, 모든 조건이 null이 발생 시 전체 엔티티를 불러오게 되므로 대장애가 발생할 수 있음)
출처 : https://velog.io/@seho100/QueryDSL-BooleanExpression%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%8F%99%EC%A0%81%EC%BF%BC%EB%A6%AC
5) 서비스 인터페이스에 상속
package com.shop.repository;
import com.shop.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import java.util.List;
// extends JpaRepository<엔티티 타입 클래스, 기본기 타입>
public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item>, ItemRepositoryCustom {
List<Item> findByItemNm(String itemNm);
@Query("select i from Item i where i.itemDetail like %:itemDetail% order by i.price desc")
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
@Query(value = "select * from Item i where i.item_detail like CONCAT('%',:itemDetail,'%') order by i.price desc", nativeQuery = true)
List<Item> findByItemDetailByNative(@Param("itemDetail") String itemDetail);
}
- QuerydslPredicateExecutor
스프링 데이터는 QuerydslPredicateExecutor라는 인터페이스를 제공합니다.
(1) Predicate에 매칭되는 하나의 Entity를 반환합니다.
(2) Predicate에 매칭되는 모든 Entity를 반환합니다.
(3) Predicate에 매칭되는 Entity의 수를 반환합니다.
(4) Predicate에 매칭되는 결과가 있는지 여부를 반환합니다.
이 외에도 정렬 정보를 전달한다든지, Page 를 반환하게 하는 인터페이스도 존재합니다.
공식문서 : https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions.querydsl
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.data.querydsl; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import java.util.Optional; import java.util.function.Function; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; public interface QuerydslPredicateExecutor<T> { Optional<T> findOne(Predicate predicate); Iterable<T> findAll(Predicate predicate); Iterable<T> findAll(Predicate predicate, Sort sort); Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders); Iterable<T> findAll(OrderSpecifier<?>... orders); Page<T> findAll(Predicate predicate, Pageable pageable); long count(Predicate predicate); boolean exists(Predicate predicate); <S extends T, R> R findBy(Predicate predicate, Function<FetchableFluentQuery<S>, R> queryFunction); }
6) 서비스 구현
package com.shop.service;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemImgDto;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
private final ItemImgService itemImgService;
private final ItemImgRepository itemImgRepository;
...
@Transactional(readOnly = true)
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){
return itemRepository.getAdminItemPage(itemSearchDto, pageable);
}
...
}
7) 컨트롤러 구현
package com.shop.controller;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemSearchDto;
import com.shop.entity.Item;
import com.shop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityNotFoundException;
import javax.validation.Valid;
import java.util.List;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class ItemContoller {
private final ItemService itemService;
...
@GetMapping(value = {"/admin/items", "/admin/items/{page}"})
public String itemManage(ItemSearchDto itemSearchDto, @PathVariable("page") Optional<Integer> page, Model model){
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 3);
Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);
model.addAttribute("items", items);
model.addAttribute("itemSearchDto", itemSearchDto);
model.addAttribute("maxPage", 5);
return "item/itemMng";
}
...
}
8) itemMng.html 작성
소스가 길기때문에 아래링크 참고
결과)
반응형