[TIL] 23.07.05 QueryDsl 이용한 동적 검색조건 처리 + Paging (8)

hyewon jeong·2023년 7월 6일
0

TIL

목록 보기
136/138

0. 들어가며

검색조건을 다중선택 하여 동적으로 검색하기 위해 QueryDsl의 페이징 과 sort기능으로 구현해보았다.

검색 조건은
item 의
1. name
2. priceGoe ( 가격 이상 )
3. priceLoe ( 가격 이하 )

정렬 속성은
1. name
2. price
3. createDate

내가 기존에 만들었던 정렬 검색 보면
3개를 구분하는 법을 몰라서 제목+내용 하나의 조건으로 정렬

1. QueryDsl 설정(boot 2.7)

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

1-1 . Gradle - build - clean

자동으로 생성된 큐클래스를 제거 할때 클릭한다. 처음 생성할때는 안전하게 clean 을 하는 편이다.

1-2. Gradle - orther - compile.java

아래 그림과 같이
other > compileJava 또는 클릭하면 Q파일이 생성된다.
만약 other > compileQueryDslJava 버튼이 만들어 지지 않는다면
other > compileQueryDslJava 클릭으로도 가능하다.
build > build 클릭으로도 가능하다.

1-3. Q 타입 생성 확인

build > generated >querydsl> study.querydsl.entity.QHello.java 파일이 생성되어 있어야 함

참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다. 앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다. (대부분 gradle build 폴더를 git에 포함하지 않는다.)

2. Controller

/**
   * 검색 기능 조회 . 동적인 검색 기능 + 페이징 (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를 통해 다시 쉼표로 구분지어 배열로 저장 후 각각의 값에 저장했다.

3. Dto

3-1. ItemOrderCond

@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";
  }
}

3-2. ItemSearchCond

처음에는 아래와 같이 별도로 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;
  }
}

4. Service

 /**
   *  검색 조건에 따른 상품 조건 ( 동적인 조건)
   * @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);
  }

5. Repository

5-1. 사용자 정의 레포지토리

5-1-1. 사용자 정의 인터페이스 작성 (검색조건으로 검색 및 페이징 처리)

public interface SearchItemCustom {
  Page searchItemByDynamicCond(PageRequest page, ItemSearchCond itemSearchCond);
}

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

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;
  }


}

6. 테스트 결과

api/users/items/search?page=1&itemSearchCondStr=ALBUM, , &itemOrderCondStr= , 

6-1. name : ALBUM 를 포함한 아이템 검색

" " 값으로 null 값 처리 되어 createdDate 기준으로 내림차순 정렬 되는 것을 볼 수 있다.

6-2. name : 무비무비 를 포함하고, 가격은 3000원 이하인 것을 검색, 정렬은 가격을 기준으로 내림차순

/api/users/items/search?page=1&itemSearchCondStr=무비무비, ,3000&itemOrderCondStr=DESC,price

profile
개발자꿈나무

0개의 댓글