스프링부트의 Querydsl 설정은 다음 주소를 참조.
1.★스프링부트 JPA Querydsl Maven 설정 및 페이징처리
중요한 부분은
페이지 블럭 처리를 위한 3. Pagination 소스 이용과
8.BoardRepositoryCustomImpl 에서 querydsl 사용법
그리고 controller 부분만 이해하면 될것 같다.
소스 : https://github.com/braverokmc79/Springboot-JPA-Blog/tree/master
1. Board
import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.CreationTimestamp; import com.querydsl.core.annotations.QueryProjection; import javax.persistence.*; import java.time.LocalDateTime; import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor @Builder @Entity //User 클래스가 MySQL 에 테이블이 생성이 된다. public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY)//기본키 생성을 데이터베이스에 위임 한다. auto_increment private Long id; @Column(nullable = false, length =100) private String title; //섬머노트 라이브러리 <html> 태그가 섞여서 디자인됨. @Lob //대용량 데이터 private String content; //@ColumnDefault("0") private int count;//조회수 /** * Member와 Team 사이가 다대일 @ManyToOne 관계로 매핑되어 있는 상황에서,* * @ManyToOne 어노테이션에 fetch 타입을 줄 수 있다.* * FetchType.LAZY * 무에서는 가급적 지연 로딩만 사용하다. 즉시 로딩 쓰지 말자. * * JPA 구현체도 한번에 가저오려고 하고, 한번에 가져와서 쓰면 좋지 않나? * 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. * *즉시 로딩(Eager Loading)과 지연 로딩(Lazy Loading) * '즉시 로딩'은 불필요한 조인까지 포함해서 처리하는 경우가 많기 때문에 '지연 로딩'의 사용을 권장하고 있습니다. * * 각 연관관계의 default 속성은 다음과 같습니다.* * @OneToMany: LAZY * @ManyToOne: EAGER * @ManyToMany: LAZY * @OneToOne: EAGER ⌨️ 즉시 로딩(Eager Loading)과 지연 로딩(Lazy Loading)의 주의할 점 가급적이면 지연 로딩(Lazy Loading)만 사용(특히 실무에서) 즉시 로딩(Eager Loading)을 적용하면 예상하지 못한 SQL이 발생할 수 있음 즉시 로딩(Earge Loading)은 JPQL에서 N+1 문제를 일으킴 * */ @ManyToOne(fetch = FetchType.LAZY) //Many=Many, User=One @JoinColumn(name="userId") private User user; //DB 는 오브젝트를 저장활 수 없다. FK, 자바는 오브젝트를 저장할 수 있다.1 //mappedBy 연관관계의 주인이 아니다 난 FK 가 아니에요. DB에 컬럼을 만들지 마세요. /** * @ManyToOne * @JoinColumn(name = "boardId") * private Board board; * mappedBy 에는 board 를 적는다. */ //Column으로 쓰지않는 변수에 대한 선언. @Transient //@Transient @OneToMany(mappedBy = "board", fetch = FetchType.LAZY) //하나의 게시판에 여러개의 댓글이 존재 , 따라서 oneToMany 의 기본전략은 LAZY 이다. private List<Reply> reply; @CreationTimestamp private LocalDateTime createDate; //등록일 }
2. BoardDto
import java.time.LocalDateTime; import com.cos.blog.model.User; import com.querydsl.core.annotations.QueryProjection; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString @NoArgsConstructor public class BoardDto { private Long id; private String title; private int count;//조회수 private User user; private LocalDateTime createDate; //등록일 @QueryProjection public BoardDto(Long id, String title, int count, User user, LocalDateTime createDate) { super(); this.id = id; this.title = title; this.count = count; this.user = user; this.createDate = createDate; } }
3. Pagination
import groovy.transform.ToString; import lombok.Data; @Data @ToString public class Pagination { /** 한 페이지당 게시글 수 **/ private int pageSize = 10; /** 한 블럭(range)당 페이지 수 **/ private int rangeSize = 10; /** 현재 페이지 **/ private int curPage = 1; /** 현재 블럭(range) **/ private int curRange = 1; /** 총 게시글 수 **/ private int listCnt; /** 총 페이지 수 **/ private int pageCnt; /** 총 블럭(range) 수 **/ private int rangeCnt; /** 시작 페이지 **/ private int startPage = 1; /** 끝 페이지 **/ private int endPage = 1; /** 시작 index **/ private int startIndex = 0; /** 이전 페이지 **/ private int prevPage; /** 다음 페이지 **/ private int nextPage; public Pagination(int listCnt, int curPage, int pageSize , int rangeSize ) { this.pageSize=pageSize; this.rangeSize=rangeSize; /** * 페이징 처리 순서 1. 총 페이지수 2. 총 블럭(range)수 3. range setting */ // 총 게시물 수와 현재 페이지를 Controller로 부터 받아온다. /** 현재페이지 **/ setCurPage(curPage); /** 총 게시물 수 **/ setListCnt(listCnt); /** 1. 총 페이지 수 **/ setPageCnt(listCnt); /** 2. 총 블럭(range)수 **/ setRangeCnt(pageCnt); /** 3. 블럭(range) setting **/ rangeSetting(curPage); } public void setPageCnt(int listCnt) { this.pageCnt = (int) Math.ceil(listCnt * 1.0 / pageSize); } public void setRangeCnt(int pageCnt) { this.rangeCnt = (int) Math.ceil(pageCnt * 1.0 / rangeSize); } public void rangeSetting(int curPage) { setCurRange(curPage); this.startPage = (curRange - 1) * rangeSize + 1; this.endPage = startPage + rangeSize - 1; if (endPage > pageCnt) { this.endPage = pageCnt; } this.prevPage = curPage - 1; this.nextPage = curPage + 1; } public void setCurRange(int curPage) { this.curRange = (int) ((curPage - 1) / rangeSize) + 1; } }
4. SearchCond
import org.springframework.web.util.UriComponentsBuilder; import lombok.Data; @Data public class SearchCond { /** * 현재 시간과 등록일을 * all : 등록일 전체 * 1d: 최근 하루 동안 * 1w : 최근 일주일 * 1m : 최근 한달 * 6m : 최근 6개월 */ private String searchDateType; /** * 검색 유형 * 제목 (title), 작성자(username) , 내용(content) */ private String searchType; /** * 조회할 검색어 저장할 변수입니다. */ private String keyword=""; public String getBoardsLink() { UriComponentsBuilder builder=UriComponentsBuilder.fromPath("") .queryParam("searchDateType", searchDateType) .queryParam("searchType", searchType) .queryParam("keyword", keyword); return builder.toUriString(); } }
5. BoardController
import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; 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.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import com.cos.blog.dto.BoardDto; import com.cos.blog.model.SearchCond; import com.cos.blog.service.BoardService; import com.cos.blog.util.Pagination; @Controller public class BoardController { @Autowired private BoardService boardService; //@PageableDefault(size = 1, sort = "id", direction = Sort.Direction.DESC) Pageable pageable @GetMapping(value={"", "/"}) public String index(SearchCond searchCond , @RequestParam(value = "page",defaultValue ="1", required = false) Optional<Integer> page, Model model) { //페이징을 위해서 PageRequest.of 메소드를 통해 Pageable 객체를 생성합니다. //첫 번째 파라미터로 조회할 페이지 번호, 두 번째 파라미터로 한 번에 가지고 올 데이터 수를 넣어줍니다. //URL 경로에 페이지 번호가 있으면 해당 페이지르 조회하도록 세팅하고, 페이지 번호가 없으면 0페이지를 조회하도록 합니다. Pageable pageable= PageRequest.of(page.isPresent()? page.get()-1 :0, 3); Page<BoardDto> boards= boardService.boardSearchList(searchCond , pageable); //페이지 블럭 계산을 위한 처리 //전체 페이지, 현재 페이지, 한 페이지당 게시글 수, 한 블럭(range)당 페이지 수 Pagination pagination =new Pagination((int)boards.getTotalElements(), page.get(), boards.getSize() , 5); model.addAttribute("boards", boards); model.addAttribute("searchCond", searchCond); model.addAttribute("pagination", pagination); return "index"; } }
6. BoardService
@Service @RequiredArgsConstructor @Transactional public class BoardService private final BoardRepository boardRepository; @Transactional(readOnly = true) public Page<BoardDto> boardSearchList(SearchCond searchCond , Pageable pageable) { return boardRepository.boardSearchList(searchCond ,pageable); } }
7. BoardRepositoryCustom
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import com.cos.blog.dto.BoardDto; import com.cos.blog.model.SearchCond; public interface BoardRepositoryCustom { Page<BoardDto> boardSearchList(SearchCond searchCond, Pageable pageable); }
8. BoardRepositoryCustomImpl
import java.time.LocalDateTime; import java.util.List; import javax.persistence.EntityManager; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.thymeleaf.util.StringUtils; import com.cos.blog.dto.BoardDto; import com.cos.blog.dto.QBoardDto; import com.cos.blog.model.Board; import com.cos.blog.model.QBoard; import com.cos.blog.model.SearchCond; import com.querydsl.core.QueryResults; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; public class BoardRepositoryCustomImpl implements BoardRepositoryCustom { /** 동적으로 쿼리를 생성하기 위해서 JPAQueryFactory 클래스를 사용합니다. */ private JPAQueryFactory queryFactory; /** JPAQueryFactory 생성자로 EntityManager 객체를 넣어줍니다. */ public BoardRepositoryCustomImpl(EntityManager em){ this.queryFactory =new JPAQueryFactory(em); } @Override public Page<BoardDto> boardSearchList(SearchCond searchCond, Pageable pageable) { QBoard board =QBoard.board; System.out.println("pageable offset : " +pageable.getOffset()); QueryResults<BoardDto> results=queryFactory.select( new QBoardDto( board.id, board.title, board.count, board.user, board.createDate) ) .from(board) .where( regDtsAfter(searchCond.getSearchDateType()), searchByLike(searchCond.getSearchType(), searchCond.getKeyword()) ) .orderBy(board.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); List<BoardDto> content=results.getResults(); long total=results.getTotal(); //조회한 데이터를 page 클래스의 구현체인 Pageimpl 객체롤 반환합니다. return new PageImpl<>(content, pageable,total); } private BooleanExpression searchByLike(String searchType, String keyword){ if(StringUtils.equals("title", searchType)){ return QBoard.board.title.like("%"+keyword+"%"); }else if (StringUtils.equals("username", searchType)){ return QBoard.board.user.username.like("%"+keyword+"%"); }else if(StringUtils.equals("content", searchType)) { return QBoard.board.content.like("%"+keyword+"%"); } return null; } private BooleanExpression regDtsAfter(String searchDataType){ LocalDateTime dateTime=LocalDateTime.now(); if(StringUtils.equals("all", searchDataType) || searchDataType ==null || searchDataType.equals("")){ return null; }else if(StringUtils.equals("1d",searchDataType)){ dateTime=dateTime.minusDays(1); }else if(StringUtils.equals("1w", searchDataType)){ dateTime=dateTime.minusWeeks(1); }else if(StringUtils.equals("1m", searchDataType)){ dateTime=dateTime.minusMonths(1); }else if(StringUtils.equals("6m",searchDataType)){ dateTime=dateTime.minusMonths(6); }else{ return null; } return QBoard.board.createDate.after(dateTime); } }
9. BoardRepository
public interface BoardRepository extends JpaRepository<Board, Long> , QuerydslPredicateExecutor<Item> , BoardRepositoryCustom{ }
10.view
thymeleaf 처리
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout=http://www.ultraq.net.nz/thymeleaf/layout layout:decorate="~{layouts/layout1}"> <div layout:fragment="content"> <div class="container"> <form> <div class="form-inline justify-content-center" th:object="${searchCond}"> <select class="form-control mr-3" name="searchType" th:field="*{searchType}"> <option value="title">제목</option> <option value="username">작성자</option> <option value="content">내용</option> </select> <input type="text" class="form-control mr-3" th:field="*{keyword}" placeholder="검색어를 입력해주세요." size="50"> <select th:field="*{searchDateType}" class="form-control mr-3" style="width: auto;"> <option value="all">전체기간</option> <option value="1d">1일</option> <option value="1w">1주</option> <option value="1m">1개월</option> <option value="6m">6개월</option> </select> <button type="submit" class="btn btn-primary">검색</button> </div> </form> <div th:each="board : ${boards}" class="card m-2"> <div class="card-body"> <h4 class="card-title" th:text="${board.title}"></h4> <a href="#" class="btn btn-primary">상세보기</a> </div> </div> [[${searchCond.getBoardsLink()}]] <br> 첫번째 페이지 : [[${boards.first}]] <br> [[${pagination.listCnt}]] 개 <br> [[${pagination}]] <br> 현재 페이지 :[[${pagination.curPage}]] <ul class="pagination justify-content-center" th:if="${pagination.listCnt>0}"> <th:block th:if="${not boards.first}"> <li class="page-item" ><a class="page-link" th:href="@{${searchCond.getBoardsLink()} +'&page=1'}">≪</a></li> <li class="page-item" ><a class="page-link" th:href="@{${searchCond.getBoardsLink()}+'&page='+${pagination.prevPage}}"><</a></li> </th:block> <li class="page-item" th:each="page : ${ #numbers.sequence(pagination.startPage, pagination.endPage) }" th:classappend="${page eq pagination.curPage} ? 'active':'' "> <a class="page-link" th:href="@{${searchCond.getBoardsLink()}+'&page=' +${page}}">[[${page}]]</a> </li> <th:block th:if="${not boards.last}"> <li class="page-item"><a class="page-link" th:href="@{${searchCond.getBoardsLink()}+'&page=' + ${pagination.nextPage}}">></a></li> <li class="page-item"><a class="page-link" th:href="@{${searchCond.getBoardsLink()}+'&page=' + ${boards.totalPages}}">≫</a></li> </th:block> </ul> </div> </div> </html>
JSP 처리
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ include file="layout/header.jsp" %> <div class="container"> <form> <div class="form-inline justify-content-center" th:object="${searchCond}"> <select class="form-control mr-3" name="searchType" > <option value="title" ${searchCond.searchType eq 'title' ? 'selected':''} >제목</option> <option value="username" ${searchCond.searchType eq 'username' ? 'selected':''} >작성자</option> <option value="content" ${searchCond.searchType eq 'content' ? 'selected':''} >내용</option> </select> <input type="text" class="form-control mr-3" name="keyword" value="${searchCond.keyword}" placeholder="검색어를 입력해주세요." size="50"> <select class="form-control mr-3" name="searchDateType" style="width: auto;"> <option value="all" >전체기간</option> <option value="1d" ${searchCond.searchDateType eq '1d' ? 'selected' :'' } >1일</option> <option value="1w" ${searchCond.searchDateType eq '1w' ? 'selected' :'' }>1주</option> <option value="1m" ${searchCond.searchDateType eq '1m' ? 'selected' :'' }>1개월</option> <option value="6m" ${searchCond.searchDateType eq '6m' ? 'selected' :'' }>6개월</option> </select> <button type="submit" class="btn btn-primary">검색</button> </div> </form> <c:forEach items="${boards.content}" var="board"> <div class="card m-2"> <div class="card-body"> <h4 class="card-title">${board.title}</h4> <a href="#" class="btn btn-primary">상세보기</a> </div> </div> </c:forEach> [[${searchCond.getBoardsLink()}]]<br> 첫번째 페이지 : [[${boards.first}]] <br> [[${pagination.listCnt}]] 개 <br> [[${pagination}]] <br> 현재 페이지 :[[${pagination.curPage}]] <ul class="pagination justify-content-center"> <c:if test="${not boards.first}"> <li class="page-item"> <a class="page-link" href="${searchCond.getBoardsLink()}&page=1">≪</a> </li> <li class="page-item"> <a class="page-link" href="${searchCond.getBoardsLink()}&page=${pagination.prevPage}"><</a> </li> </c:if> <c:forEach begin="${pagination.startPage}" end="${pagination.endPage}" var="page" step="1" varStatus="status"> <li class="page-item ${page eq pagination.curPage ? 'active' : ''}"> <a class="page-link" href="${searchCond.getBoardsLink()}&page=${page}">${page}</a> </li> </c:forEach> <c:if test="${not boards.last}"> <li class="page-item"><a class="page-link" href="${searchCond.getBoardsLink()}&page=${pagination.nextPage}">></a></li> <li class="page-item"><a class="page-link" href="${searchCond.getBoardsLink()}&page=${boards.totalPages}">≫</a></li> </c:if> </ul> </div> <%@ include file="layout/footer.jsp" %>
pageable 샘플 값
/* "content": [ ], "pageable": { "sort": { "sorted": true, "unsorted": false, "empty": false }, "pageNumber": 0, "pageSize": 1, "offset": 0, "paged": true, "unpaged": false }, "last": false, "totalElements": 4, "totalPages": 4, "first": true, "numberOfElements": 1, "size": 1, "number": 0, "sort": { "sorted": true, "unsorted": false, "empty": false }, "empty": false */
댓글 ( 4)
댓글 남기기