GuestBook 프로젝트 (9) (QuerydslRepositorySupport, 검색 (search) 처리 및 적용)

2022. 12. 3. 19:42· 코드로 배우는 스프링 부트 웹 프로젝트/Guestbook
728x90

GuestBook 프로젝트 (8) (조회 / 수정 / 삭제 처리, Controller 화면 처리)

 

GuestBook 프로젝트 (8) (조회 / 수정 / 삭제 처리, Controller 화면 처리)

GuestBook 프로젝트 (7) (Join 처리, JPQL 적용, DTO, Service, ServcieImpl) GuestBook 프로젝트 (7) (Join 처리, JPQL 적용, DTO, Service, ServcieImpl) GuestBook 프로젝트 (6) (Member / Board / Reply 생성, 연관 관계 추가, 지연(Lazy)

soohykeee.tistory.com

 

 

 


 

 

이번에는 검색기능을 추가해줄 것이다. 앞서 Guestbook에서 사용했던 검색기능 보다 더 확장된 기능을 사용할 것이다. 

Repository 확장 방법

Spring Data JPA의 Repository를 확장하기 위해서는 다음과 같은단계로 처리가 된다.

  • 쿼리 메소드나 @Query 등으로 처리할 수 없는 기능은 별도의 인터페이스로 설계
  • 별도의 인터페이스에 대한 구현 클래스를 작성한다. 이때 @QuerydslRepositorySupport라는 클래스를 부모 클래스로 사용
  • 구현 클래스에 인터페이스의 기능을 Q도메인 클래스와 JPQLQuery를 이용해서 구현

QuerydslRepositorySupport 클래스는 Spring Data JPA에 포함된 클래스로 Querydsl 라이브러리를 이용해서 직접 무언가를 구현할 때 사용한다. 

 

 

 

search 인터페이스 추가

repository 하위 디렉토리에 search 디렉토리를 추가해준 후, Board를 검색하는 기능을 가진 SearchBoardRepository 인터페이스, 해당 인터페이스를 implements 받는 SearchBoardRepositoryImpl 클래스를 생성해준다.
또한 BoardRepository에서 검색을 하는 것이므로 SearchBoardRepository를 상속받도록 수정해줘야한다.
해당 파일들이 제대로 실행이 되는지 확인하기 위해 아래와 같이 코드를 작성한 후, test파일을 생성해서 확인해보자.

package com.example.guestbookdemo.repository.search;

import com.example.guestbookdemo.entity.Board;

public interface SearchBoardRepository {

    Board search1();
    
}
package com.example.guestbookdemo.repository.search;

import com.example.guestbookdemo.entity.Board;
import com.example.guestbookdemo.entity.QBoard;
import com.querydsl.jpa.JPQLQuery;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

import java.util.List;

@Log4j2
public class SearchBoardRepositoryImpl extends QuerydslRepositorySupport implements SearchBoardRepository {

    public SearchBoardRepositoryImpl() {
        super(Board.class);
    }

    @Override
    public Board search1() {

        QBoard board = QBoard.board;

        JPQLQuery<Board> jpqlQuery = from(board);

        jpqlQuery.select(board).where(board.bno.eq(1L));

        List<Board> result = jpqlQuery.fetch();

        return null;
    }
}

 

package com.example.guestbookdemo.repository;

import com.example.guestbookdemo.entity.Board;
import com.example.guestbookdemo.repository.search.SearchBoardRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface BoardRepository extends JpaRepository<Board, Long>, SearchBoardRepository {

    //... 생략

}
    @Test
    public void testSearch1() {
        boardRepository.search1();
    }

 

query 정상실행

 

SearchBoardRepositoryImpl 에서 작성한 search1() 메소드의 내용이 콘솔창에 정상적으로 쿼리가 나온것을 볼수있다. 

 

JPQLQuery

JPQLQuery로 다른 엔티티와 조인을 처리하기 위해서는 join(), leftJoin(), rightJoin() 등을 이용할 수 있고, 필요한 경우에는 on()을 이용하여 조인에 필요한 부분을 완성할 수도 있다. 이를 이용하여 Board, Reply, Member를 적절하게 조인해줘야 한다. 또한 groupBy() 등을 이용하여 집합 함수로 처리하는 것도 가능하다.

 

해당 기능을 앞서 작성했던 search() 메소드에 적용하여 아래와 같이 코드를 수정해야한다.

    @Override
    public Board search1() {

        QBoard board = QBoard.board;
        QReply reply = QReply.reply;
        QMember member = QMember.member;

        JPQLQuery<Board> jpqlQuery = from(board);
        jpqlQuery.leftJoin(member).on(board.writer.eq(member));
        jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

        jpqlQuery.select(board, member.email, reply.count()).groupBy(board);

        List<Board> result = jpqlQuery.fetch();

        return null;
    }

 

board 테이블에 member, reply를 조건에 맞춰 조인시켜준 후, 해당 jpqlQuery문에서 board, 작성자 이메일, 댓글 수를 board로 group화 해서 출력해주도록 쿼리를 추가해준다.

위의 코드를 test메소드에서 실행해보면 아래와 같이 쿼리가 작성한대로 나가는 것을 확인할 수 있다.

query 정상실행

 

 

 


 

JPQLQuery로 Page<Object[]> 처리

위의 쿼리의 결과를 보면 Board 객체와, 작성자의 이메일, 댓글의 개수가 출력되는 것을 확인할 수 있다. 이제 해당 정보를 가져올 수 있으니 해당 데이터를 파라미터(Pageable)를 전송하고, Page<Object[]>를 만들어서 반환하는 것이다. 해당 정보를 담은 searchPage() 메소드를 만들어주면 된다.

package com.example.guestbookdemo.repository.search;

import com.example.guestbookdemo.entity.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface SearchBoardRepository {

    Board search1();

    Page<Object[]> searchPage(String type, String keyword, Pageable pageable);

}
package com.example.guestbookdemo.repository.search;

import com.example.guestbookdemo.entity.*;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPQLQuery;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

import java.util.List;

@Log4j2
public class SearchBoardRepositoryImpl extends QuerydslRepositorySupport implements SearchBoardRepository {

    public SearchBoardRepositoryImpl() {
        super(Board.class);
    }

    @Override
    public Board search1() {

        //... 생략
    }

    @Override
    public Page<Object[]> searchPage(String type, String keyword, Pageable pageable) {

        QBoard board = QBoard.board;
        QReply reply = QReply.reply;
        QMember member = QMember.member;

        JPQLQuery<Board> jpqlQuery = from(board);
        jpqlQuery.leftJoin(member).on(board.writer.eq(member));
        jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

        JPQLQuery<Tuple> tuple = jpqlQuery.select(board, member, reply.count());

        BooleanBuilder booleanBuilder = new BooleanBuilder();
        BooleanExpression expression = board.bno.gt(0L);

        booleanBuilder.and(expression);

        if (type != null) {
            String[] typeArr = type.split("");

            BooleanBuilder conditionBuilder = new BooleanBuilder();

            for (String t : typeArr) {
                switch (t) {
                    case "t":
                        conditionBuilder.or(board.title.contains(keyword));
                        break;
                    case "w":
                        conditionBuilder.or(member.email.contains(keyword));
                        break;
                    case "c":
                        conditionBuilder.or(board.content.contains(keyword));
                        break;
                }
            }
            booleanBuilder.and(conditionBuilder);
        }
        tuple.where(booleanBuilder);
        
        tuple.groupBy(board);
        
        List<Tuple> result = tuple.fetch();
        
        return null;
    }


}

 

searchPage에 type, keyword, pageable을 파라미터로 전달해주는데 PageRequestDTO를 사용하지 않고 저렇게 작성해준 이유는, 되도록이면 repository 쪽에서 DTO를 다루지 않는 편이 좋기때문이다.

결과반환

  • fetch : 조회 대상이 여러건일 경우. 컬렉션 반환
  • fetchOne : 조회 대상이 1건일 경우(1건 이상일 경우 에러). generic에 지정한 타입으로 반환
  • fetchFirst : 조회 대상이 1건이든 1건 이상이든 무조건 1건만 반환. 내부에 보면 return limit(1).fetchOne() 으로 되어있음
  • fetchCount : 개수 조회. long 타입 반환
  • fetchResults : 조회한 리스트 + 전체 개수를 포함한 QueryResults 반환. count 쿼리가 추가로 실행된다.

 

@Test
public void testSearchPage() {
    Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());

    Page<Object[]> result = boardRepository.searchPage("t", "1", pageable);
}

 

query 정상실행

 

 


 

sort / count 처리

Pageable의 Sort 객체는 JPQLQuery의 orderBy()의 파라미터로 전달되야 하지만 JPQL에서는 Sort 객체를 지원하지 않기 떄문에 orderBy()의 경우 OrderSpecifier<T extedns Comparable>을 파라미터로 처리해야 한다. 그렇기에 아래처럼 코드가 복잡하게 나온다.

package com.example.guestbookdemo.repository.search;

import com.example.guestbookdemo.entity.*;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

import java.util.List;
import java.util.stream.Collectors;

@Log4j2
public class SearchBoardRepositoryImpl extends QuerydslRepositorySupport implements SearchBoardRepository {

    public SearchBoardRepositoryImpl() {
        super(Board.class);
    }

    @Override
    public Board search1() {

        //...생략
    }

    @Override
    public Page<Object[]> searchPage(String type, String keyword, Pageable pageable) {

        QBoard board = QBoard.board;
        QReply reply = QReply.reply;
        QMember member = QMember.member;

        JPQLQuery<Board> jpqlQuery = from(board);
        jpqlQuery.leftJoin(member).on(board.writer.eq(member));
        jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

        JPQLQuery<Tuple> tuple = jpqlQuery.select(board, member, reply.count());

        BooleanBuilder booleanBuilder = new BooleanBuilder();
        BooleanExpression expression = board.bno.gt(0L);

        booleanBuilder.and(expression);

        if (type != null) {
            String[] typeArr = type.split("");

            BooleanBuilder conditionBuilder = new BooleanBuilder();

            for (String t : typeArr) {
                switch (t) {
                    case "t":
                        conditionBuilder.or(board.title.contains(keyword));
                        break;
                    case "w":
                        conditionBuilder.or(member.email.contains(keyword));
                        break;
                    case "c":
                        conditionBuilder.or(board.content.contains(keyword));
                        break;
                }
            }
            booleanBuilder.and(conditionBuilder);
        }
        tuple.where(booleanBuilder);

        Sort sort = pageable.getSort();

        sort.stream().forEach(order -> {
            Order direction = order.isAscending() ? Order.ASC : Order.DESC;
            String prop = order.getProperty();

            PathBuilder orderByExpression = new PathBuilder(Board.class, "board");
            tuple.orderBy(new OrderSpecifier(direction, orderByExpression.get(prop)));
        });

        tuple.groupBy(board);

        tuple.offset(pageable.getOffset());
        tuple.limit(pageable.getPageSize());

        List<Tuple> result = tuple.fetch();

        long count = tuple.fetchCount();

        return new PageImpl<Object[]>(
                result.stream().map(t -> t.toArray()).collect(Collectors.toList()), pageable, count
        );
    }


}

 

테스트 코드에서는 고의적으로 중첩되는 Sort 조건을 만들어서 추가해주었다. 실행 결과를 보면 order by 조건이 만들어진 것과, 목록을 위한 sql과 count 처리를 위한 sql이 실행이 되는 것을 확인할 수 있다. 콘솔에 출력 된 결과는 너무 길어 캡쳐하지 못했다.

 

 


 

검색 적용

적용하는데는 큰 어려움은 없다. 우선 boardServiceImpl 에서 getList 코드를 조금 수정해줘야한다. 그전에는 board와 reply count 정보만 가져오는 result를 사용했다면, 지금은 pageRequestDTO에서 검색하는 type, keyword값을 가져온 후 bno 순으로 정렬해서 result에 넣어주고 return 해주면 된다.

@Override
public PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO) {
    Function<Object[], BoardDTO> fn = (en -> entityToDTO((Board) en[0], (Member) en[1], (Long) en[2]));

    //Page<Object[]> result = boardRepository.getBoardWithReplyCount(pageRequestDTO.getPageable(Sort.by("bno").descending()));

    Page<Object[]> result = boardRepository.searchPage(
            pageRequestDTO.getType(),
            pageRequestDTO.getKeyword(),
            pageRequestDTO.getPageable(Sort.by("bno").descending())
    );

    return new PageResultDTO<>(result, fn);
}

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">
        <h1 class="mt-4">
            <!-- 생략 -->
        </h1>

        <form action="/board/list" method="get" id="searchForm">
            <div class="input-group">
                <input type="hidden" name="page" value="1">
                <div class="input-group-prepend">
                    <select class="custom-select" name="type">
                        <option th:selected="${pageRequestDTO.type == null}">-------</option>
                        <option value="t" th:selected="${pageRequestDTO.type == 't'}">제목</option>
                        <option value="c" th:selected="${pageRequestDTO.type == 'c'}">내용</option>
                        <option value="w" th:selected="${pageRequestDTO.type == 'w'}">작성자</option>
                        <option value="tc" th:selected="${pageRequestDTO.type == 'tc'}">제목+내용</option>
                        <option value="tcw" th:selected="${pageRequestDTO.type == 'tcw'}">제목+내용+작성자</option>
                    </select>
                </div>
                <input class="form-control" name="keyword" th:value="${pageRequestDTO.keyword}">
                <div class="input-group-append" id="button-addon4">
                    <button class="btn btn-outline-secondary btn-search" type="button">Search</button>
                    <button class="btn btn-outline-secondary btn-clear" type="button">Clear</button>
                </div>
            </div>
        </form>

        <table class="table table-striped">
            <!-- 생략 -->
        </table>

        <ul class="pagination h-100 justify-content-center align-items-center">
            <!-- 생략 -->
        </ul>


        <script th:inline="javascript">
            var msg = [[${msg}]];
            console.log(msg);

            if (msg) {
                $(".modal").modal();
            }

            var searchForm = $("#searchForm");

            $('.btn-search').click(function (e) {
                searchForm.submit();
            });

            $('.btn-clear').click(function (e) {
                searchForm.empty().submit();
            });

        </script>

    </th:block>
</th:block>
</html>

 

 

위처럼 작성해준 후 board/list 에서 검색을 해보면 정상적으로 검색이 되는 것을 확인할 수 있다.

검색 성공

 

 

 

 


 

 

728x90
저작자표시 (새창열림)
'코드로 배우는 스프링 부트 웹 프로젝트/Guestbook' 카테고리의 다른 글
  • GuestBook 프로젝트 (마지막) (Reply 댓글 추가 / 수정 / 삭제 적용, @RestController)
  • GuestBook 프로젝트 (8) (조회 / 수정 / 삭제 처리, Controller 화면 처리)
  • GuestBook 프로젝트 (7) (Join 처리, JPQL 적용, DTO, Service, ServcieImpl)
  • GuestBook 프로젝트 (6) (Member / Board / Reply 생성, 연관 관계 추가, 지연(Lazy) / 즉시(Eager) 로딩, Fetch)
soohykeee
soohykeee
Computer Science. 2017~2023 / Java, Spring, Backend
soohykeee
Coding_
soohykeee
전체
오늘
어제
  • 분류 전체보기
    • 회고
    • Info
      • 개념 정리
      • 정보
    • Study
      • Pet-Clinic-Project
      • Concept
    • Inflearn
      • 스프링 핵심 원리_기본편
      • Git
    • 코드로 배우는 스프링 부트 웹 프로젝트
      • Guestbook
      • MovieReview
      • Security & API

블로그 메뉴

  • 홈
  • 방명록
  • Github

인기 글

최근 댓글

최근 글

250x250
hELLO · Designed By 정상우.v4.2.1
soohykeee
GuestBook 프로젝트 (9) (QuerydslRepositorySupport, 검색 (search) 처리 및 적용)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.