검색조건을 다중선택 하여 동적으로 검색하기 위해 QueryDsl의 페이징 과 sort기능으로 구현해보았다.
검색 조건은
item 의
1. name
2. priceGoe ( 가격 이상 )
3. priceLoe ( 가격 이하 )
정렬 속성은
1. name
2. price
3. createDate
내가 기존에 만들었던 정렬 검색 보면
3개를 구분하는 법을 몰라서 제목+내용 하나의 조건으로 정렬
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.13'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'study'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = "11"
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
// H2
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
//validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
//redis를 사용하기 위한 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
//jwt
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
//자바 역직렬화 문제 해결 패키지
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'com.fasterxml.jackson.core:jackson-databind'
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
useJUnitPlatform()
}
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
targetCompatibility = JavaVersion.VERSION_11
자동으로 생성된 큐클래스를 제거 할때 클릭한다. 처음 생성할때는 안전하게 clean 을 하는 편이다.
아래 그림과 같이
other > compileJava 또는 클릭하면 Q파일이 생성된다.
만약 other > compileQueryDslJava 버튼이 만들어 지지 않는다면
other > compileQueryDslJava 클릭으로도 가능하다.
build > build 클릭으로도 가능하다.
build > generated >querydsl> study.querydsl.entity.QHello.java 파일이 생성되어 있어야 함
참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다. 앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다. (대부분 gradle build 폴더를 git에 포함하지 않는다.)
/**
* 검색 기능 조회 . 동적인 검색 기능 + 페이징 (QueryDsl 의 OrderSpecifier + page 이용 ) V2
* 검색조건 : name, priceGoe , priceLoe 3가지
*/
@GetMapping("/items/search")
public Page<ItemResponse> searchItemByDynamicCond(
@RequestParam(value = "page", required = false, defaultValue = "1") int page,
@RequestParam(value = "size", required = false, defaultValue = "5") int size,
@RequestParam(value = "itemOrderCondStr", required = false) String itemOrderCondStr,
@RequestParam(value = "itemSearchCondStr", required = false) String itemSearchCondStr) {
ItemOrderCond itemOrderCond = new ItemOrderCond();
if (StringUtils.hasText(itemOrderCondStr)) {
String[] value = itemOrderCondStr.split(",");
itemOrderCond.setDirection(value[0]); // null 경우 DESC 반환
itemOrderCond.setProperties(value[1]); // null 경우 createdDate 반환
}
ItemSearchCond itemSearchCond = new ItemSearchCond();
if (StringUtils.hasText(itemSearchCondStr)) {
// 문자열 값을 ItemSearchCond 객체에 설정
String[] values = itemSearchCondStr.split(",");
itemSearchCond.setName(values[0]);
itemSearchCond.setPriceGoe(values[1]);
itemSearchCond.setPriceLoe(values[2]);
}
return itemService.searchItemByDynamicCond(page, size, itemOrderCond, itemSearchCond);
}
Dto의 값을 어떻게 저장해서 가져올까 하다가
Dto 로 바로 값을 가져오는 것이 아닌 원하는 값을 각각
itemOrderCondStr 과 itemSearchCondStr 으로 받아와 Dto에 setter를 이용해 값을 저장해 주었다.
각각의 조건들은 쉼표(,)로 구분지어서 itemXXXXCondStr로 값을 받아온 후 split를 통해 다시 쉼표로 구분지어 배열로 저장 후 각각의 값에 저장했다.
@Getter
@NoArgsConstructor
public class ItemOrderCond {
private String direction;
private String properties;
public void setDirection(String direction) {
this.direction = StringUtils.hasText(direction)? direction : "DESC";
}
public void setProperties(String properties) {
this.properties = StringUtils.hasText(properties)? properties : "createdDate";
}
}
처음에는 아래와 같이 별도로 null 값을 처리 하지 않았다. 그랬더니 default값이 별도로 없어서 에러가 나는 것이다. 그래서 default 로 null 값을 반환하게 하여 추후 where절에서 null 은 무시됨으로 조건에서 무시되도록 하였다.
@Getter
@NoArgsConstructor
public class ItemSearchCond {
// 아이템명 , 가격 ( 이상 , 이하 )
private String name;
private Integer priceGoe;
private Integer priceLoe;
public void setName(String name) {
this.name = StringUtils.hasText(name) ? name : null;
}
public void setPriceGoe(String priceGoe) {
this.priceGoe = StringUtils.hasText(priceGoe) ? Integer.parseInt(priceGoe): null;
}
public void setPriceLoe(String priceLoe) {
this.priceLoe = StringUtils.hasText(priceLoe) ? Integer.parseInt(priceLoe): null;
}
}
/**
* 검색 조건에 따른 상품 조건 ( 동적인 조건)
* @param page
* @param size
* @param itemOrderCond
* @param itemSearchCond
* @return
*/
public Page<ItemResponse> searchItemByDynamicCond(int page, int size, ItemOrderCond itemOrderCond, ItemSearchCond itemSearchCond) {
return itemRepository.searchItemByDynamicCond(PageRequest.of(page-1,size,Sort.Direction.fromString(itemOrderCond.getDirection()),
itemOrderCond.getProperties()),itemSearchCond);
}
public interface SearchItemCustom {
Page searchItemByDynamicCond(PageRequest page, ItemSearchCond itemSearchCond);
}
package study.wonyshop.item.repository;
import static study.wonyshop.item.entity.QItem.item;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import javax.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.StringUtils;
import study.wonyshop.item.dto.ItemSearchCond;
import study.wonyshop.item.entity.Item;
/**
* 규칙 : 이렇게 해야 spring data Jpa 가 인식함 itemRepository or 사용자정의인터페이스 명 + Impl
*/
public class SearchItemCustomImpl implements SearchItemCustom {
private final JPAQueryFactory jpaQueryFactory;
public SearchItemCustomImpl(EntityManager em) {
this.jpaQueryFactory = new JPAQueryFactory(em);
}
@Override
public Page searchItemByDynamicCond(PageRequest page,
ItemSearchCond itemSearchCond) {
List<Item> content = jpaQueryFactory
.selectFrom(item) //QItem.item static import
.where(nameContains(itemSearchCond.getName()),
priceGoe(itemSearchCond.getPriceGoe()),
priceLoe(itemSearchCond.getPriceLoe()))
.offset(page.getOffset())
.limit(page.getPageSize())
.orderBy(itemSort(page))
.fetch();
JPAQuery<Item> countQuery = jpaQueryFactory
.selectFrom(item) //QItem.item static import
.offset(page.getOffset())
.limit(page.getPageSize())
.orderBy(itemSort(page));
// CountQuery 최적화 //PageableExecutionUtils.getPage()로 최적화
return PageableExecutionUtils.getPage(content, page, countQuery::fetchCount);
}
private BooleanExpression nameContains(String name) {
return StringUtils.hasText(name) ? item.name.contains(name) : null;
}
private BooleanExpression priceGoe(Integer priceGoe) {
return priceGoe == null ? null : item.price.goe(priceGoe);
}
private BooleanExpression priceLoe(Integer priceLoe) {
return priceLoe == null ? null : item.price.loe(priceLoe);
}
/*** 정렬조건
* OrderSpecifier 를 쿼리로 반환하여 정렬조건을 맞춰준다
* 리스트 정렬
*/
private OrderSpecifier<?> itemSort(PageRequest page) {
//서비스에서 보내준 Pageable 객체에 정렬조건 null 값 체크
if (!page.getSort().isEmpty()) {
//정렬값이 들어 있으면 for 를 사용하여 Sort.Order 객체 리스트로 정렬정보를 가져온다.
for (Sort.Order order : page.getSort()) {
// 서비스에서 넣어준 DESC or ASC 를 가져온다.
Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
// 서비스에서 넣어준 정렬 조건을 스위치 케이스 문을 활용하여 셋팅하여 준다.
switch (order.getProperty()) {
case "name":
return new OrderSpecifier<>(direction, item.name);
case "price":
return new OrderSpecifier<>(direction, item.price);
case "createdDate":
return new OrderSpecifier<>(direction, item.createdDate);
}
}
}
return null;
}
}
api/users/items/search?page=1&itemSearchCondStr=ALBUM, , &itemOrderCondStr= ,
" " 값으로 null 값 처리 되어 createdDate 기준으로 내림차순 정렬 되는 것을 볼 수 있다.
/api/users/items/search?page=1&itemSearchCondStr=무비무비, ,3000&itemOrderCondStr=DESC,price