스프링

 

 

스프링부트의  Querydsl 설정은 다음 주소를 참조.

1.★스프링부트 JPA Querydsl Maven 설정 및 페이징처리

 

2.[JPA] QueryDSL 정리

 

중요한 부분은

페이지 블럭 처리를 위한  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}}">&lt;</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}}">&gt;</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}">&lt;</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}">&gt;</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
    
   */

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

If he waits long enough, the world will be his own. (참고 충분히 기다려라.)

댓글 ( 4)

댓글 남기기

작성