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클래스파일 생성
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되지 않을 경우
- Gradle Task에 가서 다시 CompileQuerydsl 더블 클릭
- Gradle Task에 가서 querydsl에서 CleanQuerydslSourcesDir 더블 클릭 후 다시 CompileQuerydsl 더블 클릭
- Setting > Build Tools > Gradle 에서
Build and run using과 Run test using을 default값인 Gradle에서 IntelliJ IDEA로 변경- 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";
}
}
게시판 목록 구현 부분을 해당 코드로 적용
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">««</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">«</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">»</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">»»</span>
</a>
</li>
</ul>
</nav>
</nav>
</form>
</div>
</html>
페이징
검색
좋은 정보 얻어갑니다, 감사합니다.