[Spring JPA] 7. QueryDsl 설정 및 게시판 페이징, 검색 구현

YB·2023년 7월 28일
0

JPA

목록 보기
8/12

1. QueryDsl 적용

build.gradle

buildscript {
	dependencies {
		classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:1.0.10")
	}
}

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.9'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

apply plugin: "com.ewerk.gradle.plugins.querydsl"

group = 'jpa'
version = '0.0.1-SNAPSHOT'
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-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'
	implementation 'com.querydsl:querydsl-jpa'
	implementation 'com.querydsl:querydsl-apt'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

def querydslDir = 'src/main/generated'

querydsl {
	library = "com.querydsl:querydsl-apt"
	jpa = true
	querydslSourcesDir = querydslDir
}

sourceSets{
	main{
		java{
			srcDirs = ['src/main/java', querydslDir]
		}
	}
}

compileQuerydsl{
	options.annotationProcessorPath = configurations.querydsl
}

configurations {
	querydsl.extendsFrom compileClasspath
}

tasks.named('test') {
	useJUnitPlatform()
}

gradle에 빨간 네모를 친 부분을 추가

QuerydslConfiguration.java

package jpa.board.configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QuerydslConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }

}

jpa > board > configuration이란 폴더를 만들고 QuerydslConfiguration.java파일 추가

Gradle Task에 있는 compileQuerydsl 더블 클릭

프로젝트 > src > main > generated > Querydsl 폴더에 Q클래스파일 생성

2. 게시판 페이징 목록 구현

BoardDto.java

package jpa.board.dto;

import com.querydsl.core.annotations.QueryProjection;
import jpa.board.entity.Board;
import jpa.board.entity.Member;
import lombok.Data;

import javax.validation.constraints.NotEmpty;
import java.time.LocalDateTime;

@Data
public class BoardDto {

    private Long id;            //시퀀스
    @NotEmpty(message = "제목은 필수입니다.")
    private String title;              //제목
    private String content;            //내용
    private LocalDateTime regDate;     //등록 날짜
    private LocalDateTime uptDate;     //수정 날짜
    private Long viewCount;            //조회수
    private String username;            //사용자 이름

    public BoardDto(String title, String content){
        this.title = title;
        this.content = content;
    }

    @QueryProjection
    public BoardDto(Long id, String title, String content, LocalDateTime regDate , LocalDateTime uptDate, Long viewCount, String username) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.regDate = regDate;
        this.uptDate = uptDate;
        this.viewCount = viewCount;
        this.username = username;
    }

    public Board toEntity(Member member){
        return Board.builder()
                .member(member)
                .title(title)
                .content(content)
                .build();
    }
    
}

만약에 @NotEmpty이 Importe되지 않는다면 gradle > dependencies에

implementation 'org.springframework.boot:spring-boot-starter-validation'

문구를 추가하고 새로고침합니다.

스프링부트 2.3버전 이후부터는 저 문구를 추가하여야 사용가능합니다.

저번에 만들어놓은 BoardDto.java에 문구를 추가합니다.

repository패키지에 BoardRepository 인터페이스 생성
repositoryImpl패키지 생성 후에 BoardRepositoryImpl 클래스 생성

BoardRepository.java

package jpa.board.repository;

import jpa.board.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board, Long> {

}

BoardRepositoryImpl.java

package jpa.board.repositoryImpl;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jpa.board.dto.BoardDto;
import jpa.board.dto.QBoardDto;
import jpa.board.repository.CustomBoardRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import java.util.List;

import static jpa.board.entity.QBoard.board;
import static jpa.board.entity.QMember.member;

@Repository
public class BoardRepositoryImpl implements CustomBoardRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public BoardRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public Page<BoardDto> selectBoardList(String searchVal, Pageable pageable) {
        List<BoardDto> content = getBoardMemberDtos(searchVal, pageable);
        Long count = getCount(searchVal);
        return new PageImpl<>(content, pageable, count);
    }

    private Long getCount(String searchVal) {
        Long count = jpaQueryFactory
                .select(board.count())
                .from(board)
                //.leftjoin(board.member, member) //검색조건 최적화
                .fetchOne();
        return count;
    }

    private List<BoardDto> getBoardMemberDtos(String searchVal, Pageable pageable) {
        List<BoardDto> content = jpaQueryFactory
                .select(new QBoardDto(
                         board.id
                        ,board.title
                        ,board.content
                        ,board.regDate
                        ,board.uptDate
                        ,board.viewCount
                        ,member.username))
                .from(board)
                .leftJoin(board.member, member)
                .where(containsSearch(searchVal))
                .orderBy(board.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        return content;
    }

    private BooleanExpression containsSearch(String searchVal) {
        return searchVal != null ? board.title.contains(searchVal) : null;
    }
}

만약 Q클래스파일이 import되지 않을 경우

  1. Gradle Task에 가서 다시 CompileQuerydsl 더블 클릭
  2. Gradle Task에 가서 querydsl에서 CleanQuerydslSourcesDir 더블 클릭 후 다시 CompileQuerydsl 더블 클릭
  3. Setting > Build Tools > Gradle 에서
    Build and run using과 Run test using을 default값인 Gradle에서 IntelliJ IDEA로 변경
  4. File > Project Structure > Project Setting > Module 에서 프로젝트의 main에서 generated 폴더 좌클릭 Sources 설정 후 Apply

네 가지 방법을 적용해보기

BoardController.java

package jpa.board.controller;

import jpa.board.dto.BoardDto;
import jpa.board.repository.CustomBoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
public class BoardController {

    private final CustomBoardRepository customBoardRepository;

    @GetMapping("/")
    public String list(String searchVal, Pageable pageable, Model model) {
        Page<BoardDto> results = customBoardRepository.selectBoardList(searchVal, pageable);
        model.addAttribute("list", results);
        model.addAttribute("maxPage", 5);
        model.addAttribute("searchVal", searchVal);

        pageModelPut(results, model);
        return "board/list";
    }

    private void pageModelPut(Page<BoardDto> results, Model model) {
        model.addAttribute("totalCount", results.getTotalElements());
        model.addAttribute("size", results.getPageable().getPageSize());
        model.addAttribute("number", results.getPageable().getPageNumber());
    }

    @GetMapping("/write")
    public String write() {
        return "board/write";
    }

    @GetMapping("/update")
    public String upadte() {
        return "board/update";
    }
}

게시판 목록 구현 부분을 해당 코드로 적용

3. 게시판 목록 구현 html

list.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layout/default_layout">
<div layout:fragment="content" class="content">
  <form th:action th:object="${form}" method="get">
    <nav class="container">
      <br>
      <div class="input-group">
        <input type="text" name="searchVal" th:value="${searchVal}" class="form-control" placeholder="제목을 입력해주세요.">
        <button type="submit" class="btn btn-secondary">검색</button>
      </div>
      <br>
      <table class="table table-hover">
        <colgroup>
          <col width="2%" />
          <col width="5%" />
          <col width="20%" />
          <col width="5%" />
          <col width="5%" />
          <col width="5%" />
        </colgroup>
        <thead>
        <tr>
          <th>
            <label class="checkbox-inline">
              <input type="checkbox" id="allCheckBox" class="chk">
            </label>
          </th>
          <th>번호</th>
          <th>제목</th>
          <th>작성자</th>
          <th>날짜</th>
          <th>조회수</th>
        </tr>
        </thead>

        <tbody>
        <tr th:each="list, index : ${list}">
          <td>
            <label class="checkbox-inline">
              <input type="checkbox" class="chk" name="cchk" value="">
            </label>
          <td th:text="${totalCount - (size * number) - index.index}"></td>
          <td><a th:text="${list.title}" href=""></a></td>
          <td th:text="${list.username}"></td>
          <td th:text="${#temporals.format(list.regDate, 'yyyy-MM-dd')}"></td>
          <td th:text="${list.viewCount}"></td>
        </tr>
        </tbody>
      </table>
      <br>

      <div class="d-flex justify-content-end">
        <a class="btn btn-danger">글삭제</a>
        <a href="/write" class="btn btn-primary">글쓰기</a>
      </div>
      <br>
      <nav class="container d-flex align-items-center justify-content-center" aria-label="Page navigation example"
           th:with="start=${(list.number/maxPage)*maxPage + 1},
                      end=(${(list.totalPages == 0) ? 1 : (start + (maxPage - 1) < list.totalPages ? start + (maxPage - 1) : list.totalPages)})">
        <ul class="pagination">

          <li th:if="${start > 1}" class="page-item">
            <a th:href="@{/?(page=0, searchVal=${searchVal})}" class="page-link" href="#" aria-label="Previous">
              <span aria-hidden="true">&laquo;&laquo;</span>
            </a>
          </li>

          <li th:if="${start > 1}" class="page-item">
            <a th:href="@{/?(page=${start - maxPage-1}, searchVal=${searchVal})}" class="page-link" href="#" aria-label="Previous">
              <span aria-hidden="true">&laquo;</span>
            </a>
          </li>

          <li th:each="page: ${#numbers.sequence(start, end)}" class="page-item" th:classappend="${list.number+1 == page} ? active">
            <a th:href="@{/?(page=${page-1}, searchVal=${searchVal})}" th:text="${page}" class="page-link" href="#">1</a>
          </li>

          <li th:if="${end < list.totalPages}" class="page-item">
            <a th:href="@{/?(page=${start + maxPage -1}, searchVal=${searchVal})}" class="page-link" href="#" aria-label="Next">
              <span aria-hidden="true">&raquo;</span>
            </a>
          </li>

          <li th:if="${end < list.totalPages}" class="page-item">
            <a th:href="@{/?(page=${list.totalPages-1}, searchVal=${searchVal})}" class="page-link" href="#" aria-label="Next">
              <span aria-hidden="true">&raquo;&raquo;</span>
            </a>
          </li>
        </ul>
      </nav>
    </nav>
  </form>
</div>
</html>

4. 결과 화면

페이징

검색

profile
개인이 공부한걸 작성하는 블로그입니다..

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기