Study/Java Spring Boot

QueryDSL기능 사용방법 및 설정 (검색기능) 심화

kahaha 2023. 3. 12. 21:03

검색기능 사용을 위한 기본편 이후 controller부에서 사용하기 위한 service로직 구현까지의 내용이다

 

 

@RepositoryRestResource // spring data rest 사용하기 위함
public interface ArticleRepository extends
        JpaRepository<Article, Long>,
        QuerydslPredicateExecutor<Article>, //해당 엔티티 안에있는 모든 검색기능을 추가해줌 // 완전 동일해야만 동작
        QuerydslBinderCustomizer<QArticle> // 부분검색, 대소문자 구분 등을 위함
{
    Page<Article> findByTitleContaining(String title, Pageable pageable); //제목검색
    Page<Article> findByContentContaining(String content, Pageable pageable); //내용검색/ Containing-부분검색
//    Page<Article> findByUserAccount_UserIdContaining(String userId, Pageable pageable); //회원id검색
//    Page<Article> findByUserAccount_NicknameContaining(String nickName, Pageable pageable); //닉네임검색
    Page<Article> findByHashtag(String hashtag, Pageable pageable); //제목검색

    @Override
    default void customize(QuerydslBindings bindings, QArticle root){
        //Article에 대해 선택적인 필드에 대한 검색
        bindings.excludeUnlistedProperties(true); //리스팅 하지 않은 프로퍼티 검색 제외
        bindings.including(root.title, root.content, root.hashtag, root.createdAt, root.createdBy); //검색 컬럼
        bindings.bind(root.title).first(StringExpression::containsIgnoreCase); // like '%${v}%'/ 부분검색
        bindings.bind(root.content).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.hashtag).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.createdAt).first(DateTimeExpression::eq);//원하는 날짜검색 /시분초 동일하게 넣어야함
        bindings.bind(root.createdBy).first(StringExpression::containsIgnoreCase);
    }
}

* Repository

레파지토리 부분에 서비스 로직에 사용할 메서드를 추가하였다.

Containing은 부분검색을 가능하게 만들어준다.

 

@Slf4j
@RequiredArgsConstructor
@Transactional //import annotation 유의해서 가져오기
@Service
public class ArticleService {
    private final ArticleRepository articleRepository;
    //게시글 페이지형 검색
    @Transactional(readOnly = true) // 변경하지 않기때문에 readonly
    public Page<Article> searchArticles(SearchType searchType, String search_keyword, Pageable pageable) {
        // 검색어 없이 검색하면 게시글 페이지를 반환.
        if (search_keyword == null || search_keyword.isBlank()) {
            return articleRepository.findAll(pageable);
        }
        // 항목에 따른 검색 - 조회
        switch (searchType) { // return이 되는 switch ->와 함께 사용 java14이상부터 사용가능
            case TITLE:
                return articleRepository.findByTitleContaining(search_keyword, pageable);
            case CONTENT:
                return articleRepository.findByContentContaining(search_keyword, pageable);
//            case ARTICLE_ID:
//                return articleRepository.findByUserAccount_UserIdContaining(search_keyword, pageable);
//            case NICKNAME:
//                return articleRepository.findByUserAccount_NicknameContaining(search_keyword, pageable);
            case HASHTAG:
                return articleRepository.findByHashtag(search_keyword, pageable);
        }
        return null;
    }

* Service

일치하는 검색어가 없으면 전체 게시글 페이지를 반환하고 있으면 해당하는 페이지만 반환하도록 로직을 구성하였다.

 

 

public enum SearchType {
    TITLE, CONTENT, ARTICLE_ID, NICKNAME, HASHTAG
}

 

엔티티 검색을 하기위해 필요한 컬럼명 등록

 

 

@RequiredArgsConstructor
@Validated
@RequestMapping("/articles")
@RestController
public class ArticleController {

    private final ArticleService articleService;
    private final ArticleMapper articleMapper;



    @GetMapping //부분검색 //http://localhost:8080/articles?searchType=CONTENT&searchValue=검색어
    public ResponseEntity searchArticles(@RequestParam(required = false)SearchType searchType,//required = false- 선택적 파라미터
                           @RequestParam(required = false)String searchValue,
                           @PageableDefault(size = 10,sort = "createdAt", direction = Sort.Direction.DESC)Pageable pageable)
    {
        Page<Article> articlePages = articleService.searchArticles(searchType, searchValue, pageable);
        List<Article> articles = articlePages.getContent();
        List<ArticleDto.Response> response = articleMapper.articleToArticleListResponse(articles);
        return new ResponseEntity(response, HttpStatus.OK);
    }

* Controller

부분검색기능을 받는 컨트롤러에 Getmapping부

@PageableDefault를 통해 pageble을 입력해주지 않으면 디폴트값이 들어가게 된다.

 

content내용 검색을 위한 파라미터 입력 예) searchType=CONTENT&searchValue=검색내용

 

 

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ArticleMapper {
    Article articlePostToArticle(ArticleDto.Post articlePostDto);
    Article articlePatchToArticle(ArticleDto.Patch articlePatchDto);
    ArticleDto.Response articleToArticleResponse(Article article);
    List<ArticleDto.Response> articleToArticleListResponse(List<Article> articles);
}

* Mapper

컨트롤러 부에서 엔티티를 ResponseDto로 변환할 매퍼 검색기능에서는 페이지를 반환하기 위해

List<response>가  사용된다.

 

 

@Getter
@ToString(callSuper = true) //callSuper = true - 안쪽까지 들어가서 ToString을 찍어냄
@Table(indexes = {
        @Index(columnList = "title"),
        @Index(columnList = "hashtag"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy"),
})
@Entity
@NoArgsConstructor//(access = AccessLevel.PROTECTED) //기본생성자의 무분별한 생성을 막아서 의도치않는 엔티티 생성을 막음
public class Article extends AuditingFields {
    /* @Setter을 클래스레벨로 설정하지 않는 이유 id, 시간 등 을 Setter 하지 않기 위함*/
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long articleId;
   // @Setter @ManyToOne(optional = false)
    //private UserAccount userAccount; //유저 정보(id) 매핑
    @Setter @Column(nullable = false)
    private String title; // 제목
    @Setter @Column(nullable = false, length = 10000)
    private String content; // 본문
    @Setter
    private String hashtag; // 해시태그

    @OrderBy("createdAt DESC") // 생성 시간순 정렬
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)// 모든경우 외래키를 걸어줌/ 실무에선 양방향바인딩 지양
    @ToString.Exclude //조회하는 과정에서 무한 순환참조 방지, 퍼포먼스 이슈로 인하여 설정 / 연결고리를 끊어줌
    private final Set<ArticleComment> articleComments = new LinkedHashSet<>();

    public Article(String title, String content, String hashtag) { //도메인과 관련있는 정보만 오픈
        //this.userAccount = userAccount;
        this.title = title;
        this.content = content;
        this.hashtag = hashtag;
    }
/**
    //new를 사용하지 않고 생성자 생성하기 위함  // of는 매개변수가 뭐가필요한지 가이드해줌
    public static Article of(String title, String content, String hashtag) {
        return new Article(title, content, hashtag);
    } // @Mapper 사용 할때에는 쓸수없는 방법
**/

    //equalse 해시코드 기능 생성(alt+insert에 있음) / id만 같으면 같은게시물
    @Override //동일성 검사
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Article)) return false;
        Article article = (Article) o;
        return articleId!=null && articleId.equals(article.articleId); //데이터가 insert되기 전에 만들어진 id는 탈락
    }

    @Override
    public int hashCode() { //동일성 검사를 마친 아이디에 대해서만 해시
        return Objects.hash(articleId);
    }
}

* Entity

검색 대상 엔티티

 

 

postman URI  ex) http://localhost:8080/articles?searchType=CONTENT&searchValue=검색

- article엔티티내의 content컬럼에서 검색이라는 단어가 포함되어있는 항목들을 모두 반환한다.